Compare commits

..
Author SHA1 Message Date
ohif-bot 6e5892d202 chore(version): Update package versions [skip ci] 2024-04-05 03:10:27 +00:00
ohif-bot 9d8997ad84 chore(version): version.json [skip ci] 2024-04-05 03:10:20 +00:00
Ibrahim cea27d438d feat(advanced-roi-tools): new tools and icon updates and overlay bug fixes (#4014) 2024-04-04 23:00:11 -04:00
ohif-bot 78fd8cce86 chore(version): Update package versions [skip ci] 2024-04-05 02:53:53 +00:00
ohif-bot f7c9a5cb64 chore(version): version.json [skip ci] 2024-04-05 02:53:45 +00:00
nithin-trenser 56b1eae635 feat(measurement): Add support measurement label autocompletion (#3855) 2024-04-04 22:42:48 -04:00
ohif-bot 807afb8ce5 chore(version): Update package versions [skip ci] 2024-04-03 13:04:29 +00:00
ohif-bot 5f494598bb chore(version): version.json [skip ci] 2024-04-03 13:04:21 +00:00
Ibrahim fa38e6a07a fix(presentation-state): Iterate over map properly to restore the presentation state (#4013) 2024-04-03 08:53:29 -04:00
ohif-bot d8f66856cc chore(version): Update package versions [skip ci] 2024-04-03 03:44:14 +00:00
ohif-bot 9c39689451 chore(version): version.json [skip ci] 2024-04-03 03:44:05 +00:00
IbrahimandAlireza 9f3235ff09 feat(segmentation): Enhanced segmentation panel design for TMTV (#3988)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-04-02 23:33:04 -04:00
ohif-bot e8e209291c chore(version): Update package versions [skip ci] 2024-04-02 13:46:18 +00:00
ohif-bot bc71861619 chore(version): version.json [skip ci] 2024-04-02 13:46:10 +00:00
IbrahimandJoe Boccanfuso b7f90e3951 feat(ViewportActionMenu): window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering (#3963)
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2024-04-02 09:35:30 -04:00
ohif-bot 07301fc020 chore(version): Update package versions [skip ci] 2024-03-28 20:56:04 +00:00
ohif-bot 1203477dd4 chore(version): version.json [skip ci] 2024-03-28 20:55:57 +00:00
Alireza 158a181670 fix(new layout): address black screen bugs (#4008) 2024-03-28 16:45:11 -04:00
ohif-bot aac3546bed chore(version): Update package versions [skip ci] 2024-03-28 14:52:21 +00:00
ohif-bot 1256a4d565 chore(version): version.json [skip ci] 2024-03-28 14:52:11 +00:00
Ibrahim 617043fe0d feat(layout): new layout selector with 3D volume rendering (#3923) 2024-03-28 10:40:32 -04:00
ohif-bot 6f610b09e1 chore(version): Update package versions [skip ci] 2024-03-27 20:12:57 +00:00
ohif-bot a2e320f9f8 chore(version): version.json [skip ci] 2024-03-27 20:12:47 +00:00
Alireza 566b25a544 feat(toolbar): new Toolbar to enable reactive state synchronization (#3983) 2024-03-27 16:01:32 -04:00
ohif-bot 79d5c36bda chore(version): Update package versions [skip ci] 2024-03-25 15:44:13 +00:00
ohif-bot f47a94317d chore(version): version.json [skip ci] 2024-03-25 15:44:04 +00:00
Ibrahim 45b68e841d feat(worklist): new investigational use text (#3999) 2024-03-25 11:33:24 -04:00
ohif-bot d70aae3233 chore(version): Update package versions [skip ci] 2024-03-19 00:37:56 +00:00
ohif-bot 345b137bec chore(version): version.json [skip ci] 2024-03-19 00:37:48 +00:00
Ibrahim 9bcd1ae6f5 feat(worklist): New worklist buttons and tooltips (#3989) 2024-03-18 20:27:12 -04:00
ohif-bot 6b79dff5c1 chore(version): Update package versions [skip ci] 2024-03-18 18:38:23 +00:00
ohif-bot 88859f75d5 chore(version): version.json [skip ci] 2024-03-18 18:38:16 +00:00
Alireza feed2304c1 fix(SR display): and the token based navigation (#3995) 2024-03-18 14:27:17 -04:00
ohif-bot 08815909c7 chore(version): Update package versions [skip ci] 2024-03-15 13:52:45 +00:00
ohif-bot ed2234a67c chore(version): version.json [skip ci] 2024-03-15 13:52:34 +00:00
Pedro H. Köhler f7fe91c5f6 feat(delete measurement): icon for measurement table (#3775) 2024-03-15 09:35:29 -04:00
ohif-bot 21ec6860f2 chore(version): Update package versions [skip ci] 2024-03-08 13:38:48 +00:00
ohif-bot ac474b7f8f chore(version): version.json [skip ci] 2024-03-08 13:38:41 +00:00
EricB e485d68fd4 fix(cli): mode creation template (#3876) (#3981) 2024-03-08 08:27:37 -05:00
ohif-bot 5089229ff9 chore(version): Update package versions [skip ci] 2024-03-05 16:15:56 +00:00
ohif-bot df09176d63 chore(version): version.json [skip ci] 2024-03-05 16:15:48 +00:00
EricB bb603bd1f8 docs: Typo (#3975) 2024-03-05 11:04:28 -05:00
ohif-bot 82a4a7d377 chore(version): Update package versions [skip ci] 2024-02-28 17:25:27 +00:00
ohif-bot c27fa9c802 chore(version): version.json [skip ci] 2024-02-28 17:25:19 +00:00
EricB 4cdfdae814 fix(docs): Minor typos in hpModule.md (#3962) 2024-02-28 12:13:46 -05:00
ohif-bot 596340803a chore(version): Update package versions [skip ci] 2024-02-22 19:54:20 +00:00
ohif-bot ff4e17f5c2 chore(version): version.json [skip ci] 2024-02-22 19:54:11 +00:00
Bill Wallace 21e8a2bd0b fix(demo): Deploy issue (#3951) 2024-02-22 14:42:46 -05:00
ohif-bot 2a442e30eb chore(version): Update package versions [skip ci] 2024-02-21 18:59:56 +00:00
ohif-bot 8e8a66bd29 chore(version): version.json [skip ci] 2024-02-21 18:59:49 +00:00
Alireza b3a0faf5f5 feat(resize): Optimize resizing process and maintain zoom level (#3889) 2024-02-21 13:49:23 -05:00
ohif-bot a2a0090eeb chore(version): Update package versions [skip ci] 2024-02-14 14:02:25 +00:00
ohif-bot ea552027c5 chore(version): version.json [skip ci] 2024-02-14 14:02:17 +00:00
Ibrahim 0eac386a31 feat(errorboundary): format stack trace properly (#3931) 2024-02-14 08:51:17 -05:00
ohif-bot 40b590d75a chore(version): Update package versions [skip ci] 2024-02-05 19:01:43 +00:00
ohif-bot 01df1763ce chore(version): version.json [skip ci] 2024-02-05 19:01:34 +00:00
Igor Octaviano 5bba98ed84 fix: 🐛 Sort merge results based on default data source (input) (#3903) 2024-02-05 13:46:47 -05:00
ohif-bot 16785936d9 chore(version): Update package versions [skip ci] 2024-01-22 20:52:46 +00:00
ohif-bot 3deb73d642 chore(version): version.json [skip ci] 2024-01-22 20:52:38 +00:00
m00n620 ef46b5a704 fix toggleHpTools to support split primary button (#3900)
Fixes a small bug in toggling buttons with hanging protocols.
2024-01-22 15:42:51 -05:00
ohif-bot ed2bf23cd2 chore(version): Update package versions [skip ci] 2024-01-22 15:07:51 +00:00
ohif-bot 2ff2f3f34d chore(version): version.json [skip ci] 2024-01-22 15:07:43 +00:00
Patrick Wespi a47aeb8bd7 fix: catch errors in getPTImageIdInstanceMetadata (#3897) 2024-01-22 09:58:22 -05:00
ohif-bot 94f40192f5 chore(version): Update package versions [skip ci] 2024-01-22 14:55:27 +00:00
ohif-bot 1c43aa791a chore(version): version.json [skip ci] 2024-01-22 14:55:17 +00:00
Alireza e4a116b074 fix(viewport-sync): remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync (#3849) 2024-01-22 09:44:35 -05:00
ohif-bot 2460d28afc chore(version): Update package versions [skip ci] 2024-01-19 13:30:55 +00:00
ohif-bot 4a958b8989 chore(version): version.json [skip ci] 2024-01-19 13:30:47 +00:00
Pedro H. Köhler 31b837fa90 fix: is same orientaiton (#3905) 2024-01-19 08:21:22 -05:00
ohif-bot 9bb00d71fa chore(version): Update package versions [skip ci] 2024-01-17 16:01:31 +00:00
ohif-bot 40bbe33a9d chore(version): version.json [skip ci] 2024-01-17 16:01:24 +00:00
Igor Octaviano 911d672835 fix: 🐛 Check merge key for merge data source (#3901) 2024-01-17 10:51:49 -05:00
ohif-bot 9c553fded4 chore(version): Update package versions [skip ci] 2024-01-12 16:10:06 +00:00
ohif-bot 144dd39393 chore(version): version.json [skip ci] 2024-01-12 16:09:58 +00:00
AbishekBista9 df2f50748e docs: Update architecture.md (#3891) 2024-01-12 11:00:17 -05:00
ohif-bot 83a1523408 chore(version): Update package versions [skip ci] 2024-01-12 13:58:08 +00:00
ohif-bot 5a62a983e5 chore(version): version.json [skip ci] 2024-01-12 13:57:58 +00:00
Bill Wallace d00a86b022 fix: Update CS3D to fix second render (#3892) 2024-01-12 08:48:15 -05:00
ohif-bot 710a2dca72 chore(version): Update package versions [skip ci] 2024-01-09 19:31:20 +00:00
ohif-bot 9100b1f02e chore(version): version.json [skip ci] 2024-01-09 19:31:10 +00:00
0049f4c030 feat(hp): enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol (#3804)
Co-authored-by: rodrigobasilio2022 <rodrigo.basilio@radicalimaging.com>
Co-authored-by: rodrigobasilio2022 <114958722+rodrigobasilio2022@users.noreply.github.com>
2024-01-09 14:22:00 -05:00
ohif-bot b5cdf88a57 chore(version): Update package versions [skip ci] 2024-01-09 17:45:39 +00:00
ohif-bot c793df727b chore(version): version.json [skip ci] 2024-01-09 17:45:30 +00:00
Alireza 1456a493d6 feat(transferSyntax): prefer server transcoded transfer syntax for all images (#3883) 2024-01-09 12:36:29 -05:00
ohif-bot 7c551f5043 chore(version): Update package versions [skip ci] 2024-01-09 15:24:36 +00:00
ohif-bot 0b2c2a235f chore(version): version.json [skip ci] 2024-01-09 15:24:28 +00:00
Alireza b1efe40aa1 fix(segmentation): upgrade cs3d to fix various segmentation bugs (#3885) 2024-01-09 10:15:56 -05:00
ohif-bot a8a0bdb2df chore(version): Update package versions [skip ci] 2024-01-08 19:34:59 +00:00
ohif-bot 6878c1a264 chore(version): version.json [skip ci] 2024-01-08 19:34:49 +00:00
AbishekBista9 d181eb4fdc docs: Update user-account-control.md (#3877) 2024-01-08 14:24:42 -05:00
ohif-bot fd809e4a0b chore(version): Update package versions [skip ci] 2024-01-08 19:21:13 +00:00
ohif-bot 4c29670d76 chore(version): version.json [skip ci] 2024-01-08 19:21:04 +00:00
Igor Octaviano f58725ce40 feat: Add on mode init hook (#3882) 2024-01-08 14:12:26 -05:00
ohif-bot 5a1cb842f0 chore(version): Update package versions [skip ci] 2024-01-08 19:08:04 +00:00
ohif-bot c5bd79552a chore(version): version.json [skip ci] 2024-01-08 19:07:56 +00:00
Sofien-Sellami 61bf22c6f8 feat(ui): sidePanel expandedWidth (#3728) 2024-01-08 13:59:20 -05:00
ohif-bot 3d98548072 chore(version): Update package versions [skip ci] 2024-01-08 18:53:43 +00:00
ohif-bot bf9770a55c chore(version): version.json [skip ci] 2024-01-08 18:53:35 +00:00
Pedro H. KöhlerandIgor Octaviano 2049c0936c feat: improve disableEditing flag (#3875)
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
2024-01-08 13:45:24 -05:00
ohif-bot 7d5607073d chore(version): Update package versions [skip ci] 2024-01-08 18:24:06 +00:00
ohif-bot 1b6910bf76 chore(version): version.json [skip ci] 2024-01-08 18:23:58 +00:00
Bill WallaceandAlireza 98650302c7 fix: PDF display request in v3 (#3878)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2024-01-08 13:15:54 -05:00
Salim Kanoun bf846c94c3 fix: convert radian to degree value for mip rotation (#3881) 2024-01-08 13:09:22 -05:00
ohif-bot b5dc9047ba chore(version): Update package versions [skip ci] 2024-01-08 17:37:17 +00:00
ohif-bot 636d8b0458 chore(version): version.json [skip ci] 2024-01-08 17:37:08 +00:00
Celian-abd e8858f3eb5 fix: colormap for stack viewports via HangingProtocol (#3866) 2024-01-08 12:28:22 -05:00
ohif-bot eb7c950d7e chore(version): Update package versions [skip ci] 2023-12-15 02:56:03 +00:00
ohif-bot c6214d66b9 chore(version): version.json [skip ci] 2023-12-15 02:55:55 +00:00
Alireza 0eb502aaf0 chore(version): upgrade cornerstone3D versions to fix orientation (#3854) 2023-12-14 21:47:15 -05:00
ohif-bot 44a5a24d4f chore(version): Update package versions [skip ci] 2023-12-14 03:48:22 +00:00
ohif-bot b0bf5dc780 chore(version): version.json [skip ci] 2023-12-14 03:48:15 +00:00
Alireza 5915fada83 chore(version): upgrade cornerstone3D versions (#3853) 2023-12-13 22:39:41 -05:00
ohif-bot 2fa335c6d9 chore(version): Update package versions [skip ci] 2023-12-13 21:37:11 +00:00
ohif-bot de4ad88a09 chore(version): version.json [skip ci] 2023-12-13 21:37:03 +00:00
Sofien-Sellami 6ca13c0a4c fix(icon-style): Ensure consistent icon dimensions (#3727) 2023-12-13 16:28:20 -05:00
Pedro H. Köhler 0177b625ba feat(overlay): add inline binary overlays (#3852) 2023-12-13 16:18:49 -05:00
ohif-bot c4064acfbc chore(version): Update package versions [skip ci] 2023-12-13 20:27:22 +00:00
ohif-bot 7a05d169ef chore(version): version.json [skip ci] 2023-12-13 20:27:14 +00:00
dxlin 13d0c883f8 docs: Update HangingProtocolService.md doc sameAs example to contain constr… (#3765) 2023-12-13 15:18:15 -05:00
ohif-bot c45f60dce2 chore(version): Update package versions [skip ci] 2023-12-13 20:11:48 +00:00
ohif-bot 7bb2713c5c chore(version): version.json [skip ci] 2023-12-13 20:11:39 +00:00
Celian-abdandAlireza 97cb1f94e1 fix(dicom-seg) : Fix the missing color tag when loading Dicom-Seg (#3822)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2023-12-13 15:03:09 -05:00
ohif-bot f99645f3a1 chore(version): Update package versions [skip ci] 2023-12-13 19:55:36 +00:00
ohif-bot ae16c7f972 chore(version): version.json [skip ci] 2023-12-13 19:55:28 +00:00
Salim Kanoun 9c6e1cd305 Fix(HPService): custom image load performed is now reset to false on HP exit (#3809) 2023-12-13 14:47:12 -05:00
ohif-bot 7f0ca857fa chore(version): Update package versions [skip ci] 2023-12-13 19:29:55 +00:00
ohif-bot 920256d3a4 chore(version): version.json [skip ci] 2023-12-13 19:29:48 +00:00
Pedro H. Köhler e1f55e65f2 feat(customizationService): Enable saving and loading of private tags in SRs (#3842) 2023-12-13 14:21:46 -05:00
ohif-bot d1c07d90f0 chore(version): Update package versions [skip ci] 2023-12-13 15:29:16 +00:00
ohif-bot b1a04ecac3 chore(version): version.json [skip ci] 2023-12-13 15:29:09 +00:00
Sofien-Sellami f707b4ebc9 feat(config): Add activateViewportBeforeInteraction parameter for viewport interaction customization (#3847) 2023-12-13 10:17:48 -05:00
Pedro H. Köhler 805c53270f fix: address and improve system vulnerabilities (#3851) 2023-12-13 09:25:41 -05:00
Pavel d14a8f0199 feat(i18n): enhanced i18n support (#3761) 2023-12-11 09:43:58 -05:00
ohif-bot 5a5c4f58af chore(version): Update package versions [skip ci] 2023-12-08 14:35:30 +00:00
ohif-bot 6e5d8b691e chore(version): version.json [skip ci] 2023-12-08 14:35:23 +00:00
Joe Boccanfuso 59576d695d feat(HP): Added new 3D hanging protocols to be used in the new layout selector (#3844) 2023-12-08 09:25:57 -05:00
ohif-bot 78d4bea04d chore(version): Update package versions [skip ci] 2023-12-06 13:37:37 +00:00
ohif-bot 806c2f4b36 chore(version): version.json [skip ci] 2023-12-06 13:37:29 +00:00
Alireza 6651008fbb fix(auth): fix the issue with oauth at a non root path (#3840) 2023-12-06 08:28:02 -05:00
ohif-bot 74c98b6e9f chore(version): Update package versions [skip ci] 2023-11-28 18:18:13 +00:00
ohif-bot ecee25b1d5 chore(version): version.json [skip ci] 2023-11-28 18:18:03 +00:00
Alireza f1a67647ae fix(SM): drag and drop is now fixed for SM (#3813) 2023-11-28 13:04:30 -05:00
ohif-bot 4f9a00f872 chore(version): Update package versions [skip ci] 2023-11-27 13:36:36 +00:00
ohif-bot 7673a887e4 chore(version): version.json [skip ci] 2023-11-27 13:36:29 +00:00
Joe Boccanfuso 924affa7b5 fix(cine): Set cine disabled on mode exit. (#3812) 2023-11-27 08:27:38 -05:00
ohif-bot abac6f583e chore(version): Update package versions [skip ci] 2023-11-24 22:41:51 +00:00
ohif-bot 36ba20d6e8 chore(version): version.json [skip ci] 2023-11-24 22:41:42 +00:00
Bill Wallace 9d1884d7d8 fix: Update the CS3D packages to add the most recent HTJ2K TSUIDS (#3806) 2023-11-24 17:32:19 -05:00
ohif-bot 622f18bd3e chore(version): Update package versions [skip ci] 2023-11-24 13:56:33 +00:00
ohif-bot 8b6256e76f chore(version): version.json [skip ci] 2023-11-24 13:56:24 +00:00
Igor Octaviano c4ff2c2f09 feat: Merge Data Source (#3788)
Add the ability to merge two different series queries to generate a complete study query result.  Provides basic support for other types of merges, but those aren't yet added as full features.
2023-11-24 08:46:54 -05:00
ohif-bot 1be5613946 chore(version): Update package versions [skip ci] 2023-11-21 17:47:19 +00:00
ohif-bot 6edb1c1e4c chore(version): version.json [skip ci] 2023-11-21 17:47:12 +00:00
Pedro H. Köhler 404b0a5d53 feat(events): broadcast series summary metadata (#3798) 2023-11-21 12:38:08 -05:00
ohif-bot 786898fd63 chore(version): Update package versions [skip ci] 2023-11-21 14:15:45 +00:00
ohif-bot dfa851600c chore(version): version.json [skip ci] 2023-11-21 14:15:37 +00:00
Bill Wallace 00e751933a fix(DICOM Overlay): The overlay data wasn't being refreshed on change (#3793) 2023-11-21 09:05:25 -05:00
ohif-bot c4e22c251f chore(version): Update package versions [skip ci] 2023-11-21 02:33:40 +00:00
ohif-bot 3cf4b6dea5 chore(version): version.json [skip ci] 2023-11-21 02:33:32 +00:00
Bill Wallace 8c8924af37 fix(metadata): to handle cornerstone3D update for htj2k (#3783) 2023-11-20 21:23:42 -05:00
ohif-bot 04121e3713 chore(version): Update package versions [skip ci] 2023-11-18 01:18:27 +00:00
ohif-bot d1eef6dbdb chore(version): version.json [skip ci] 2023-11-18 01:18:18 +00:00
Joe Boccanfuso d83beb7c62 feat(docs): Added various training videos to support the OHIF CLI tools (#3794) 2023-11-17 20:08:38 -05:00
ohif-bot 7867b29742 chore(version): Update package versions [skip ci] 2023-11-15 14:46:52 +00:00
ohif-bot 4a6f1bd407 chore(version): version.json [skip ci] 2023-11-15 14:46:44 +00:00
Igor Octaviano b694228dd5 feat(url): Add SeriesInstanceUIDs wado query param (#3746) 2023-11-15 09:37:09 -05:00
ohif-bot 013068bcb0 chore(version): Update package versions [skip ci] 2023-11-13 15:52:59 +00:00
ohif-bot 3fbc23b04c chore(version): version.json [skip ci] 2023-11-13 15:52:50 +00:00
Igor Octaviano 323401418e fix: 🐛 Run error handler for failed image requests (#3773) 2023-11-13 10:42:55 -05:00
622 changed files with 34650 additions and 12083 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ version: 2.1
##
orbs:
codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.1.4
cypress: cypress-io/cypress@3.3.1
executors:
cypress-custom:
-22
View File
@@ -1,22 +0,0 @@
---
name: Codespell
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Codespell
uses: codespell-project/actions-codespell@v2
+1 -1
View File
@@ -31,6 +31,6 @@
"prettier.endOfLine": "lf",
"workbench.colorCustomizations": {},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}
+11
View File
@@ -120,6 +120,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
test: /\.wasm/,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/images/[name].[ext]',
},
},
],
},
], //.concat(vtkRules),
},
resolve: {
+590
View File
@@ -3,6 +3,596 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
### Features
* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1))
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
### Features
* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470))
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
### Bug Fixes
* **presentation-state:** Iterate over map properly to restore the presentation state ([#4013](https://github.com/OHIF/Viewers/issues/4013)) ([fa38e6a](https://github.com/OHIF/Viewers/commit/fa38e6a07a259d8cb33277922884e722414ac548))
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
### Features
* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d))
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
### Features
* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6))
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
### Features
* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e))
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
### Features
* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69))
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
### Features
* **delete measurement:** icon for measurement table ([#3775](https://github.com/OHIF/Viewers/issues/3775)) ([f7fe91c](https://github.com/OHIF/Viewers/commit/f7fe91c5f6c4f05f3f3f5f640d3a119bd40a5870))
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
### Bug Fixes
* **cli:** mode creation template ([#3876](https://github.com/OHIF/Viewers/issues/3876)) ([#3981](https://github.com/OHIF/Viewers/issues/3981)) ([e485d68](https://github.com/OHIF/Viewers/commit/e485d68fd4619ce7187113cbe59e47f9523dbcc8))
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
### Bug Fixes
* **docs:** Minor typos in hpModule.md ([#3962](https://github.com/OHIF/Viewers/issues/3962)) ([4cdfdae](https://github.com/OHIF/Viewers/commit/4cdfdae8149166cf9dc91a55c0d7f2a224e55d8f))
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
### Features
* **errorboundary:** format stack trace properly ([#3931](https://github.com/OHIF/Viewers/issues/3931)) ([0eac386](https://github.com/OHIF/Viewers/commit/0eac386a31a5d6965536360aa65a44769c1a5740))
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
### Bug Fixes
* 🐛 Sort merge results based on default data source (input) ([#3903](https://github.com/OHIF/Viewers/issues/3903)) ([5bba98e](https://github.com/OHIF/Viewers/commit/5bba98ed848bdf46b5ba4fc4708527cced3308b5))
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
### Bug Fixes
* catch errors in getPTImageIdInstanceMetadata ([#3897](https://github.com/OHIF/Viewers/issues/3897)) ([a47aeb8](https://github.com/OHIF/Viewers/commit/a47aeb8bd729dcb8d2cfc13b27a31b0dd88f11ad))
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
### Bug Fixes
* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db))
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
### Bug Fixes
* is same orientaiton ([#3905](https://github.com/OHIF/Viewers/issues/3905)) ([31b837f](https://github.com/OHIF/Viewers/commit/31b837fa90f631d4984482c6e952373fbb8bdbfc))
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
### Bug Fixes
* 🐛 Check merge key for merge data source ([#3901](https://github.com/OHIF/Viewers/issues/3901)) ([911d672](https://github.com/OHIF/Viewers/commit/911d67283536b2fe7930948f2819ea0ad66e2a32))
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
### Features
* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47))
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
### Features
* **transferSyntax:** prefer server transcoded transfer syntax for all images ([#3883](https://github.com/OHIF/Viewers/issues/3883)) ([1456a49](https://github.com/OHIF/Viewers/commit/1456a493d66c90c787b022256c9f2846afb115fc))
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
### Features
* Add on mode init hook ([#3882](https://github.com/OHIF/Viewers/issues/3882)) ([f58725c](https://github.com/OHIF/Viewers/commit/f58725ce40685f7297181ef98d81bc28420c8291))
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
### Features
* **ui:** sidePanel expandedWidth ([#3728](https://github.com/OHIF/Viewers/issues/3728)) ([61bf22c](https://github.com/OHIF/Viewers/commit/61bf22c6f80e764bdf5c3b56bb0124a95aa0f793))
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
### Features
* improve disableEditing flag ([#3875](https://github.com/OHIF/Viewers/issues/3875)) ([2049c09](https://github.com/OHIF/Viewers/commit/2049c0936c86f819604c243d3dc7b3fe971b5b2c))
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
### Bug Fixes
* convert radian to degree value for mip rotation ([#3881](https://github.com/OHIF/Viewers/issues/3881)) ([bf846c9](https://github.com/OHIF/Viewers/commit/bf846c94c378f04b9f44dcd71be3f056dbcfe0b5))
* PDF display request in v3 ([#3878](https://github.com/OHIF/Viewers/issues/3878)) ([9865030](https://github.com/OHIF/Viewers/commit/98650302c7575f0aea386e32cfc4112c378035e6))
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
### Bug Fixes
* colormap for stack viewports via HangingProtocol ([#3866](https://github.com/OHIF/Viewers/issues/3866)) ([e8858f3](https://github.com/OHIF/Viewers/commit/e8858f3eb55552f695af4a55980f9ae2e9af7291))
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
### Bug Fixes
* **icon-style:** Ensure consistent icon dimensions ([#3727](https://github.com/OHIF/Viewers/issues/3727)) ([6ca13c0](https://github.com/OHIF/Viewers/commit/6ca13c0a4cb5a95bbb52b0db902b5dbf72f8aa6e))
### Features
* **overlay:** add inline binary overlays ([#3852](https://github.com/OHIF/Viewers/issues/3852)) ([0177b62](https://github.com/OHIF/Viewers/commit/0177b625ba86760168bc4db58c8a109aa9ee83cb))
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package ohif-monorepo-root
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
### Features
* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23))
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
### Bug Fixes
* address and improve system vulnerabilities ([#3851](https://github.com/OHIF/Viewers/issues/3851)) ([805c532](https://github.com/OHIF/Viewers/commit/805c53270f243ec61f142a3ffa0af500021cd5ec))
### Features
* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5))
* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57))
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
### Features
* **HP:** Added new 3D hanging protocols to be used in the new layout selector ([#3844](https://github.com/OHIF/Viewers/issues/3844)) ([59576d6](https://github.com/OHIF/Viewers/commit/59576d695d4d26601d35c43f73d602f0b12d72bf))
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
### Bug Fixes
* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe))
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
### Bug Fixes
* **cine:** Set cine disabled on mode exit. ([#3812](https://github.com/OHIF/Viewers/issues/3812)) ([924affa](https://github.com/OHIF/Viewers/commit/924affa7b5d420c2f91522a075cecbb3c78e8f52))
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
### Features
* Merge Data Source ([#3788](https://github.com/OHIF/Viewers/issues/3788)) ([c4ff2c2](https://github.com/OHIF/Viewers/commit/c4ff2c2f09546ce8b72eab9c5e7beed611e3cab0))
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
### Features
* **events:** broadcast series summary metadata ([#3798](https://github.com/OHIF/Viewers/issues/3798)) ([404b0a5](https://github.com/OHIF/Viewers/commit/404b0a5d535182d1ae44e33f7232db500a7b2c16))
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
### Bug Fixes
* **DICOM Overlay:** The overlay data wasn't being refreshed on change ([#3793](https://github.com/OHIF/Viewers/issues/3793)) ([00e7519](https://github.com/OHIF/Viewers/commit/00e751933ac6d611a34773fa69594243f1b99082))
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
### Bug Fixes
* **metadata:** to handle cornerstone3D update for htj2k ([#3783](https://github.com/OHIF/Viewers/issues/3783)) ([8c8924a](https://github.com/OHIF/Viewers/commit/8c8924af373d906773f5db20defe38628cacd4a0))
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
### Features
* **docs:** Added various training videos to support the OHIF CLI tools ([#3794](https://github.com/OHIF/Viewers/issues/3794)) ([d83beb7](https://github.com/OHIF/Viewers/commit/d83beb7c62c1d5be19c54e08d23883f112147fe1))
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
### Features
* **url:** Add SeriesInstanceUIDs wado query param ([#3746](https://github.com/OHIF/Viewers/issues/3746)) ([b694228](https://github.com/OHIF/Viewers/commit/b694228dd535e4b97cb86a1dc085b6e8716bdaf3))
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
### Bug Fixes
* 🐛 Run error handler for failed image requests ([#3773](https://github.com/OHIF/Viewers/issues/3773)) ([3234014](https://github.com/OHIF/Viewers/commit/323401418e7ccab74655ba02f990bbe0ed4e523b))
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
+2
View File
@@ -26,6 +26,7 @@ module.exports = {
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
'@babel/plugin-transform-class-static-block',
],
env: {
test: {
@@ -45,6 +46,7 @@ module.exports = {
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
],
+1 -1
View File
@@ -1 +1 @@
fd1251f751d8147b8a78c7f4d81c67ba69769afa
cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1
@@ -3,6 +3,458 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
### Bug Fixes
* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db))
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.8.0-beta.16",
"version": "3.8.0-beta.71",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.8.0-beta.16",
"@ohif/extension-cornerstone": "3.8.0-beta.16",
"@ohif/extension-default": "3.8.0-beta.16",
"@ohif/i18n": "3.8.0-beta.16",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-default": "3.8.0-beta.71",
"@ohif/i18n": "3.8.0-beta.71",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
const { formatDate } = utils;
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props) {
@@ -15,7 +13,6 @@ function OHIFCornerstoneRTViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -126,7 +124,6 @@ function OHIFCornerstoneRTViewport(props) {
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = rtDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@@ -168,6 +165,8 @@ function OHIFCornerstoneRTViewport(props) {
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
useEffect(() => {
// I'm not sure what is this, since in RT we support Overlapping segments
// via contours
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@@ -280,20 +279,7 @@ function OHIFCornerstoneRTViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
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
@@ -307,41 +293,47 @@ function OHIFCornerstoneRTViewport(props) {
});
setIsHydrated(isHydrated);
};
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: `RT Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{rtIsLoading && (
<LoadingIndicatorTotalPercent
@@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">RTSTRUCT</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>
@@ -3,6 +3,482 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
### Features
* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d))
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
### Features
* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6))
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
+10 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.8.0-beta.16",
"version": "3.8.0-beta.71",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -24,6 +24,8 @@
"yarn": ">=1.18.0"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:dicom-seg": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -31,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.8.0-beta.16",
"@ohif/extension-cornerstone": "3.8.0-beta.16",
"@ohif/extension-default": "3.8.0-beta.16",
"@ohif/i18n": "3.8.0-beta.16",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-default": "3.8.0-beta.71",
"@ohif/i18n": "3.8.0-beta.71",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -44,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.27.3",
"@cornerstonejs/tools": "^1.27.3",
"@kitware/vtk.js": "27.3.1",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@kitware/vtk.js": "29.7.0",
"react-color": "^2.19.3"
}
}
@@ -5,6 +5,7 @@ import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
Enums as cornerstoneToolsEnums,
utilities,
} from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { classes, DicomMetadataStore } from '@ohif/core';
@@ -18,6 +19,7 @@ import {
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
@@ -45,6 +47,7 @@ const commandsModule = ({
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
} = (servicesManager as ServicesManager).services;
const actions = {
@@ -203,6 +206,9 @@ const commandsModule = ({
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
updateViewportsForSegmentationRendering({
viewportId,
@@ -218,7 +224,8 @@ const commandsModule = ({
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;
},
});
@@ -397,6 +404,60 @@ const commandsModule = ({
console.warn(e);
}
},
setBrushSize: ({ value, toolNames }) => {
const brushSize = Number(value);
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
if (toolNames?.length === 0) {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
} else {
toolNames?.forEach(toolName => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
}
});
},
setThresholdRange: ({
value,
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
}) => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolNames?.forEach(toolName => {
toolGroup.setToolConfiguration(toolName, {
strategySpecificConfiguration: {
THRESHOLD: {
threshold: value,
},
},
});
});
});
},
toggleThresholdRangeAndDynamic() {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds) {
return;
}
toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
const brushInstances = segmentationUtils.getBrushToolInstances(toolGroup.id);
brushInstances.forEach(({ configuration }) => {
const { activeStrategy, strategySpecificConfiguration } = configuration;
if (activeStrategy.startsWith('THRESHOLD')) {
const thresholdConfig = strategySpecificConfiguration.THRESHOLD;
if (thresholdConfig) {
thresholdConfig.isDynamic = !thresholdConfig.isDynamic;
}
}
});
});
},
};
const definitions = {
@@ -424,11 +485,21 @@ const commandsModule = ({
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
setBrushSize: {
commandFn: actions.setBrushSize,
},
setThresholdRange: {
commandFn: actions.setThresholdRange,
},
toggleThresholdRangeAndDynamic: {
commandFn: actions.toggleThresholdRangeAndDynamic,
},
};
return {
actions,
definitions,
defaultContext: 'SEGMENTATION',
};
};
@@ -1,16 +1,26 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui';
import PanelSegmentation from './panels/PanelSegmentation';
import SegmentationToolbox from './panels/SegmentationToolbox';
import { SegmentationPanelMode } from './types/segmentation';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
const getPanelModule = ({
commandsManager,
servicesManager,
extensionManager,
configuration,
title,
}) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = configuration => {
const [appConfig] = useAppConfig();
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
const segmentationPanelMode =
customizationService.get('segmentation.segmentationPanelMode')?.value ||
SegmentationPanelMode.Dropdown;
return (
<PanelSegmentation
@@ -20,6 +30,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
configuration={{
...configuration,
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
segmentationPanelMode: segmentationPanelMode,
}}
/>
);
@@ -27,12 +38,18 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
const wrappedPanelSegmentationWithTools = configuration => {
const [appConfig] = useAppConfig();
const segmentationPanelMode =
customizationService.get('segmentation.segmentationPanelMode')?.value ||
SegmentationPanelMode.Dropdown;
return (
<>
<SegmentationToolbox
<Toolbox
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
buttonSectionId="segmentationToolbox"
title="Segmentation Tools"
configuration={{
...configuration,
}}
@@ -43,6 +60,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
extensionManager={extensionManager}
configuration={{
...configuration,
segmentationPanelMode: segmentationPanelMode,
}}
/>
</>
@@ -1,5 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -141,7 +142,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
'@ohif/extension-cornerstone.utilityModule.common'
);
const { segmentationService } = servicesManager.services;
const { segmentationService, uiNotificationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
@@ -174,12 +175,40 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
let usedRecommendedDisplayCIELabValue = true;
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
data.rgba = data.RecommendedDisplayCIELabValue;
if (data.rgba) {
data.rgba = dicomlabToRGB(data.rgba);
} else {
usedRecommendedDisplayCIELabValue = false;
data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length];
}
}
});
if (results.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message:
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
type: 'warning',
});
}
if (!usedRecommendedDisplayCIELabValue) {
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
uiNotificationService.show({
title: 'DICOM SEG import',
message:
'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.',
type: 'warning',
duration: 5000,
});
}
Object.assign(segDisplaySet, results);
}
@@ -0,0 +1,67 @@
export function getToolbarModule({ commandsManager, servicesManager }) {
const { segmentationService, toolGroupService } = servicesManager.services;
return [
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
// Todo: we need to pass in the button section Id since we are kind of
// 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();
if (!segmentations?.length) {
return {
disabled: true,
className: '!text-common-bright !bg-black opacity-50',
disabledText: disabledText ?? 'No segmentations available',
};
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isPrimaryActive = toolNames
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
];
}
function getToolNameForButton(button) {
const { props } = button;
const commands = props?.commands || button.commands;
if (commands && commands.length) {
const command = commands[0];
const { commandOptions } = command;
const { toolName } = commandOptions || { toolName: props?.id ?? button.id };
return toolName;
}
return null;
}
@@ -5,6 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
import preRegistration from './init';
const Component = React.lazy(() => {
@@ -29,7 +30,6 @@ const extension = {
*/
id,
preRegistration,
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
@@ -38,8 +38,8 @@ const extension = {
*/
getPanelModule,
getCommandsModule,
getViewportModule({ servicesManager, extensionManager }) {
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => {
return (
<OHIFCornerstoneSEGViewport
+1 -1
View File
@@ -1,5 +1,5 @@
import { addTool, BrushTool } from '@cornerstonejs/tools';
export default function init({ configuration = {} }): void {
export default function init({ servicesManager }): void {
addTool(BrushTool);
}
@@ -1,12 +1,17 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
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,
@@ -170,6 +175,22 @@ export default function PanelSegmentation({
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(
@@ -221,59 +242,53 @@ export default function PanelSegmentation({
});
};
const SegmentationGroupTableComponent = components[configuration?.segmentationPanelMode];
return (
<>
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
<SegmentationGroupTable
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAdd}
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)
}
/>
</div>
</>
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAdd}
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)
}
/>
);
}
@@ -1,405 +0,0 @@
import React, { useCallback, useEffect, useState, useReducer } from 'react';
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
import { Types } from '@ohif/extension-cornerstone';
import { utilities } from '@cornerstonejs/tools';
const { segmentation: segmentationUtils } = utilities;
const TOOL_TYPES = {
CIRCULAR_BRUSH: 'CircularBrush',
SPHERE_BRUSH: 'SphereBrush',
CIRCULAR_ERASER: 'CircularEraser',
SPHERE_ERASER: 'SphereEraser',
CIRCLE_SHAPE: 'CircleScissor',
RECTANGLE_SHAPE: 'RectangleScissor',
SPHERE_SHAPE: 'SphereScissor',
THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush',
THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush',
};
const ACTIONS = {
SET_TOOL_CONFIG: 'SET_TOOL_CONFIG',
SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL',
};
const initialState = {
Brush: {
brushSize: 15,
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
},
Eraser: {
brushSize: 15,
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
},
Shapes: {
brushSize: 15,
mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor'
},
ThresholdBrush: {
brushSize: 15,
thresholdRange: [-500, 500],
},
activeTool: null,
};
function toolboxReducer(state, action) {
switch (action.type) {
case ACTIONS.SET_TOOL_CONFIG:
const { tool, config } = action.payload;
return {
...state,
[tool]: {
...state[tool],
...config,
},
};
case ACTIONS.SET_ACTIVE_TOOL:
return { ...state, activeTool: action.payload };
default:
return state;
}
}
function SegmentationToolbox({ servicesManager, extensionManager }) {
const { toolbarService, segmentationService, toolGroupService } =
servicesManager.services as Types.CornerstoneServices;
const [viewportGrid] = useViewportGrid();
const { viewports, activeViewportId } = viewportGrid;
const [toolsEnabled, setToolsEnabled] = useState(false);
const [state, dispatch] = useReducer(toolboxReducer, initialState);
const updateActiveTool = useCallback(() => {
if (!viewports?.size || activeViewportId === undefined) {
return;
}
const viewport = viewports.get(activeViewportId);
if (!viewport) {
return;
}
dispatch({
type: ACTIONS.SET_ACTIVE_TOOL,
payload: toolGroupService.getActiveToolForViewport(viewport.viewportId),
});
}, [activeViewportId, viewports, toolGroupService, dispatch]);
const setToolActive = useCallback(
toolName => {
toolbarService.recordInteraction({
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName,
},
},
],
});
dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName });
},
[toolbarService, dispatch]
);
/**
* sets the tools enabled IF there are segmentations
*/
useEffect(() => {
const events = [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
];
const unsubscriptions = [];
events.forEach(event => {
const { unsubscribe } = segmentationService.subscribe(event, () => {
const segmentations = segmentationService.getSegmentations();
const activeSegmentation = segmentations?.find(seg => seg.isActive);
setToolsEnabled(activeSegmentation?.segmentCount > 0);
});
unsubscriptions.push(unsubscribe);
});
updateActiveTool();
return () => {
unsubscriptions.forEach(unsubscribe => unsubscribe());
};
}, [activeViewportId, viewports, segmentationService, updateActiveTool]);
/**
* Update the active tool when the toolbar state changes
*/
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
updateActiveTool();
}
);
return () => {
unsubscribe();
};
}, [toolbarService, updateActiveTool]);
useEffect(() => {
// if the active tool is not a brush tool then do nothing
if (!Object.values(TOOL_TYPES).includes(state.activeTool)) {
return;
}
// if the tool is Segmentation and it is enabled then do nothing
if (toolsEnabled) {
return;
}
// if the tool is Segmentation and it is disabled, then switch
// back to the window level tool to not confuse the user when no
// segmentation is active or when there is no segment in the segmentation
setToolActive('WindowLevel');
}, [toolsEnabled, state.activeTool, setToolActive]);
const updateBrushSize = useCallback(
(toolName, brushSize) => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
});
},
[toolGroupService]
);
const onBrushSizeChange = useCallback(
(valueAsStringOrNumber, toolCategory) => {
const value = Number(valueAsStringOrNumber);
_getToolNamesFromCategory(toolCategory).forEach(toolName => {
updateBrushSize(toolName, value);
});
dispatch({
type: ACTIONS.SET_TOOL_CONFIG,
payload: {
tool: toolCategory,
config: { brushSize: value },
},
});
},
[toolGroupService, dispatch]
);
const handleRangeChange = useCallback(
newRange => {
if (
newRange[0] === state.ThresholdBrush.thresholdRange[0] &&
newRange[1] === state.ThresholdBrush.thresholdRange[1]
) {
return;
}
const toolNames = _getToolNamesFromCategory('ThresholdBrush');
toolNames.forEach(toolName => {
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolGroup.setToolConfiguration(toolName, {
strategySpecificConfiguration: {
THRESHOLD_INSIDE_CIRCLE: {
threshold: newRange,
},
},
});
});
});
dispatch({
type: ACTIONS.SET_TOOL_CONFIG,
payload: {
tool: 'ThresholdBrush',
config: { thresholdRange: newRange },
},
});
},
[toolGroupService, dispatch, state.ThresholdBrush.thresholdRange]
);
return (
<AdvancedToolbox
title="Segmentation Tools"
items={[
{
name: 'Brush',
icon: 'icon-tool-brush',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCULAR_BRUSH ||
state.activeTool === TOOL_TYPES.SPHERE_BRUSH,
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH),
options: [
{
name: 'Radius (mm)',
id: 'brush-radius',
type: 'range',
min: 0.5,
max: 99.5,
value: state.Brush.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'Brush'),
},
{
name: 'Mode',
type: 'radio',
id: 'brush-mode',
value: state.Brush.mode,
values: [
{ value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' },
{ value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Eraser',
icon: 'icon-tool-eraser',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCULAR_ERASER ||
state.activeTool === TOOL_TYPES.SPHERE_ERASER,
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER),
options: [
{
name: 'Radius (mm)',
type: 'range',
id: 'eraser-radius',
min: 0.5,
max: 99.5,
value: state.Eraser.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'Eraser'),
},
{
name: 'Mode',
type: 'radio',
id: 'eraser-mode',
value: state.Eraser.mode,
values: [
{ value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' },
{ value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Shapes',
icon: 'icon-tool-shape',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.CIRCLE_SHAPE ||
state.activeTool === TOOL_TYPES.RECTANGLE_SHAPE ||
state.activeTool === TOOL_TYPES.SPHERE_SHAPE,
onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SHAPE),
options: [
{
name: 'Mode',
type: 'radio',
value: state.Shapes.mode,
id: 'shape-mode',
values: [
{ value: TOOL_TYPES.CIRCLE_SHAPE, label: 'Circle' },
{ value: TOOL_TYPES.RECTANGLE_SHAPE, label: 'Rectangle' },
{ value: TOOL_TYPES.SPHERE_SHAPE, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
],
},
{
name: 'Threshold Tool',
icon: 'icon-tool-threshold',
disabled: !toolsEnabled,
active:
state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH ||
state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH,
onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH),
options: [
{
name: 'Radius (mm)',
id: 'threshold-radius',
type: 'range',
min: 0.5,
max: 99.5,
value: state.ThresholdBrush.brushSize,
step: 0.5,
onChange: value => onBrushSizeChange(value, 'ThresholdBrush'),
},
{
name: 'Mode',
type: 'radio',
id: 'threshold-mode',
value: state.activeTool,
values: [
{ value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' },
{ value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' },
],
onChange: value => setToolActive(value),
},
{
type: 'custom',
id: 'segmentation-threshold-range',
children: () => {
return (
<div>
<div className="bg-secondary-light h-[1px]"></div>
<div className="mt-1 text-[13px] text-white">Threshold</div>
<InputDoubleRange
values={state.ThresholdBrush.thresholdRange}
onChange={handleRangeChange}
minValue={-1000}
maxValue={1000}
step={1}
showLabel={true}
allowNumberEdit={true}
showAdjustmentArrows={false}
/>
</div>
);
},
},
],
},
]}
/>
);
}
function _getToolNamesFromCategory(category) {
let toolNames = [];
switch (category) {
case 'Brush':
toolNames = ['CircularBrush', 'SphereBrush'];
break;
case 'Eraser':
toolNames = ['CircularEraser', 'SphereEraser'];
break;
case 'ThresholdBrush':
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'];
break;
default:
break;
}
return toolNames;
}
export default SegmentationToolbox;
@@ -0,0 +1,4 @@
export enum SegmentationPanelMode {
Expanded = 'expanded',
Dropdown = 'dropdown',
}
@@ -180,7 +180,8 @@ function getUpdatedViewportsForSegmentation({
});
}
});
return updatedViewports;
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
@@ -1,13 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
const { formatDate } = utils;
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props) {
@@ -15,7 +13,6 @@ function OHIFCornerstoneSEGViewport(props) {
children,
displaySets,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
@@ -30,6 +27,7 @@ function OHIFCornerstoneSEGViewport(props) {
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
@@ -116,14 +114,12 @@ function OHIFCornerstoneSEGViewport(props) {
}}
onElementEnabled={onElementEnabled}
onElementDisabled={onElementDisabled}
// initialImageIndex={initialImageIndex}
></Component>
);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
direction = direction === 'left' ? -1 : 1;
const segmentationId = segDisplaySet.displaySetInstanceUID;
const segmentation = segmentationService.getSegmentation(segmentationId);
@@ -172,14 +168,6 @@ function OHIFCornerstoneSEGViewport(props) {
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
setSegIsLoading(false);
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);
@@ -256,6 +244,69 @@ function OHIFCornerstoneSEGViewport(props) {
};
}, [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
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// 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]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
isHydrated,
onStatusClick,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -291,61 +342,8 @@ function OHIFCornerstoneSEGViewport(props) {
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
};
const onStatusClick = 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
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// 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);
};
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onSegmentChange}
getStatusComponent={() => {
return _getStatusComponent({
isHydrated,
onStatusClick,
});
}}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
seriesDescription: `SEG Viewport ${SeriesDescription}`,
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing:
SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{segIsLoading && (
<LoadingIndicatorTotalPercent
@@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
switch (isHydrated) {
case true:
StatusIcon = () => <Icon name="status-alert" />;
@@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
const StatusArea = () => (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
const StatusArea = () => {
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
return (
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
<StatusIcon />
<span className="ml-1">SEG</span>
</div>
)}
</div>
);
{!isHydrated && (
<div
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
onMouseUp={onStatusClick}
>
{loadStr}
</div>
)}
</div>
);
};
return (
<>
@@ -3,6 +3,479 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
### Features
* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23))
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
+10 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.8.0-beta.16",
"version": "3.8.0-beta.71",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -23,6 +23,8 @@
"ohif-extension"
],
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
@@ -32,10 +34,10 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.8.0-beta.16",
"@ohif/extension-cornerstone": "3.8.0-beta.16",
"@ohif/extension-measurement-tracking": "3.8.0-beta.16",
"@ohif/ui": "3.8.0-beta.16",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",
"@ohif/extension-measurement-tracking": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
@@ -44,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.27.3",
"@cornerstonejs/core": "^1.27.3",
"@cornerstonejs/tools": "^1.27.3",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"classnames": "^2.3.2"
}
}
@@ -40,7 +40,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
return dataset;
};
const commandsModule = ({}) => {
const commandsModule = props => {
const { servicesManager } = props;
const { customizationService } = servicesManager.services;
const actions = {
/**
*
@@ -95,7 +97,15 @@ const commandsModule = ({}) => {
throw new Error('Invalid report, no content');
}
await dataSource.store.dicom(naturalizedReport);
const onBeforeDicomStore =
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
let dicomDict;
if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
}
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
@@ -118,13 +128,9 @@ const commandsModule = ({}) => {
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
storeContexts: [],
options: {},
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
storeContexts: [],
options: {},
},
};
@@ -171,7 +171,12 @@ function _load(displaySet, servicesManager, extensionManager) {
// Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
activeDisplaySet,
dataSource,
servicesManager
);
});
// Subscribe to new displaySets as the source may come in after.
@@ -180,12 +185,23 @@ function _load(displaySet, servicesManager, extensionManager) {
// 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, newDisplaySet, dataSource);
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
newDisplaySet,
dataSource,
servicesManager
);
});
});
}
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource,
servicesManager
) {
const { customizationService } = servicesManager.services;
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
@@ -244,7 +260,20 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
let measurement = unloadedMeasurements[j];
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
'onBeforeSRAddMeasurement'
)?.value;
if (typeof onBeforeSRAddMeasurement === 'function') {
measurement = onBeforeSRAddMeasurement({
measurement,
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
});
}
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
@@ -339,7 +339,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const textLines = this._getTextBoxLinesFromLabels(label);
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
if (!annotation.data?.handles?.textBox?.worldPosition) {
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
}
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
@@ -52,7 +52,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
data: {
label: measurement.labels,
handles: {
textBox: {},
textBox: measurement.textBox ?? {},
},
cachedStats: {
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
@@ -90,7 +90,7 @@ export default function hydrateStructuredReport(
const datasetToUse = _mapLegacyDataSet(instance);
// Use dcmjs to generate toolState.
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
// for the imageToWorld transformation. The following assumes that the order
@@ -101,6 +101,16 @@ export default function hydrateStructuredReport(
metaData
);
const onBeforeSRHydration =
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
if (typeof onBeforeSRHydration === 'function') {
storedMeasurementByAnnotationType = onBeforeSRHydration({
storedMeasurementByAnnotationType,
displaySet,
});
}
// Filter what is found by DICOM SR to measurements we support.
const mappingDefinitions = mappings.map(m => m.annotationType);
const hydratableMeasurementsInSR = {};
@@ -1,35 +1,30 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import { useAppConfig } from '@state';
const { formatDate } = utils;
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRViewport(props) {
const {
children,
dataSource,
displaySets,
viewportLabel,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const [appConfig] = useAppConfig();
const { displaySetService, cornerstoneViewportService, measurementService } =
servicesManager.services;
const {
displaySetService,
cornerstoneViewportService,
measurementService,
viewportActionCornersService,
} = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -48,6 +43,8 @@ function OHIFCornerstoneSRViewport(props) {
const [element, setElement] = useState(null);
const { viewports, activeViewportId } = viewportGrid;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
let sendTrackedMeasurementsEvent;
@@ -213,18 +210,11 @@ function OHIFCornerstoneSRViewport(props) {
direction => {
let newMeasurementSelected = measurementSelected;
if (direction === 'right') {
newMeasurementSelected++;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
}
} else {
newMeasurementSelected--;
if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
@@ -298,6 +288,46 @@ function OHIFCornerstoneSRViewport(props) {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
useEffect(() => {
viewportActionCornersService.setComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}),
indexPriority: -100,
location: viewportActionCornersService.LOCATIONS.topLeft,
},
{
viewportId,
id: 'viewportActionArrowsComponent',
index: 0,
component: (
<ViewportActionArrows
key="actionArrows"
onArrowsClick={onMeasurementChange}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
isLocked,
onMeasurementChange,
sendTrackedMeasurementsEvent,
srDisplaySet,
t,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -317,57 +347,8 @@ function OHIFCornerstoneSRViewport(props) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
SeriesNumber,
} = referencedDisplaySetMetadata;
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onArrowsClick={onMeasurementChange}
getStatusComponent={() =>
_getStatusComponent({
srDisplaySet,
viewportId,
isTracked: false,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
})
}
studyData={{
label: viewportLabel,
useAltStyling: true,
studyDate: formatDate(StudyDate),
currentSeries: SeriesNumber,
seriesDescription: SeriesDescription || '',
patientInformation: {
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
spacing:
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
scanner: ManufacturerModelName || '',
},
}}
/>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{getCornerstoneViewport()}
{childrenWithProps}
@@ -384,7 +365,6 @@ OHIFCornerstoneSRViewport.propTypes = {
viewportLabel: PropTypes.string,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
viewportLabel: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
@@ -429,6 +409,7 @@ function _getStatusComponent({
isRehydratable,
isLocked,
sendTrackedMeasurementsEvent,
t,
}) {
const handleMouseUp = () => {
sendTrackedMeasurementsEvent('HYDRATE_SR', {
@@ -437,7 +418,6 @@ function _getStatusComponent({
});
};
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
// 1 - Incompatible
+506
View File
@@ -3,6 +3,512 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05)
### Features
* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1))
# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05)
### Features
* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470))
# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03)
### Bug Fixes
* **presentation-state:** Iterate over map properly to restore the presentation state ([#4013](https://github.com/OHIF/Viewers/issues/4013)) ([fa38e6a](https://github.com/OHIF/Viewers/commit/fa38e6a07a259d8cb33277922884e722414ac548))
# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03)
### Features
* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d))
# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02)
### Features
* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1))
# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28)
### Bug Fixes
* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43))
# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28)
### Features
* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6))
# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27)
### Features
* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5))
# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18)
### Bug Fixes
* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5))
# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22)
### Bug Fixes
* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30))
# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21)
### Features
* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6))
# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22)
### Bug Fixes
* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db))
# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12)
### Bug Fixes
* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412))
# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09)
### Bug Fixes
* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6))
# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08)
### Bug Fixes
* colormap for stack viewports via HangingProtocol ([#3866](https://github.com/OHIF/Viewers/issues/3866)) ([e8858f3](https://github.com/OHIF/Viewers/commit/e8858f3eb55552f695af4a55980f9ae2e9af7291))
# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13)
### Features
* **overlay:** add inline binary overlays ([#3852](https://github.com/OHIF/Viewers/issues/3852)) ([0177b62](https://github.com/OHIF/Viewers/commit/0177b625ba86760168bc4db58c8a109aa9ee83cb))
# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13)
### Features
* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23))
# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08)
### Features
* **HP:** Added new 3D hanging protocols to be used in the new layout selector ([#3844](https://github.com/OHIF/Viewers/issues/3844)) ([59576d6](https://github.com/OHIF/Viewers/commit/59576d695d4d26601d35c43f73d602f0b12d72bf))
# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06)
### Bug Fixes
* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df))
# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24)
### Bug Fixes
* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682))
# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21)
### Bug Fixes
* **DICOM Overlay:** The overlay data wasn't being refreshed on change ([#3793](https://github.com/OHIF/Viewers/issues/3793)) ([00e7519](https://github.com/OHIF/Viewers/commit/00e751933ac6d611a34773fa69594243f1b99082))
# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13)
### Bug Fixes
* 🐛 Run error handler for failed image requests ([#3773](https://github.com/OHIF/Viewers/issues/3773)) ([3234014](https://github.com/OHIF/Viewers/commit/323401418e7ccab74655ba02f990bbe0ed4e523b))
# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13)
+12 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.8.0-beta.16",
"version": "3.8.0-beta.71",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -25,6 +25,8 @@
"access": "public"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
@@ -36,9 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.27.3",
"@ohif/core": "3.8.0-beta.16",
"@ohif/ui": "3.8.0-beta.16",
"@cornerstonejs/dicom-image-loader": "^1.67.0",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",
"dcmjs": "^0.29.12",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
@@ -52,11 +55,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.27.3",
"@cornerstonejs/core": "^1.27.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.27.3",
"@cornerstonejs/tools": "^1.27.3",
"@kitware/vtk.js": "27.3.1",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@kitware/vtk.js": "29.7.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",
@@ -10,7 +10,7 @@ import {
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog } from '@ohif/ui';
import { Notification, useViewportDialog, Types as UITypes, AllInOneMenu } from '@ohif/ui';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
import { setEnabledElement } from '../state';
@@ -22,6 +22,12 @@ import CornerstoneServices from '../types/CornerstoneServices';
import CinePlayer from '../components/CinePlayer';
import { 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';
const STACK = 'stack';
/**
@@ -101,7 +107,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
@@ -109,12 +114,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIndex,
onReady,
} = props;
const viewportId = viewportOptions.viewportId;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef();
const [appConfig] = useAppConfig();
const {
measurementService,
@@ -126,12 +133,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
cornerstoneCacheService,
viewportGridService,
stateSyncService,
viewportActionCornersService,
customizationService,
} = servicesManager.services as CornerstoneServices;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
@@ -151,6 +160,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
@@ -175,8 +186,16 @@ const OHIFCornerstoneViewport = React.memo(props => {
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
onReady?.(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
@@ -197,13 +216,18 @@ const OHIFCornerstoneViewport = React.memo(props => {
return;
}
cleanUpServices(viewportInfo);
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);
};
}, []);
@@ -265,7 +289,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
// 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 { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
@@ -344,12 +371,38 @@ const OHIFCornerstoneViewport = React.memo(props => {
};
}, [displaySets, elementRef, viewportId]);
// 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;
}
// 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,
displaySets,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
return (
<React.Fragment>
<div className="viewport-wrapper">
<ReactResizeDetector
refreshMode="debounce"
refreshRate={50} // Wait 50 ms after last move to render
onResize={onResize}
targetRef={elementRef.current}
/>
@@ -373,7 +426,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
servicesManager={servicesManager}
/>
</div>
<div className="absolute w-full">
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
@@ -385,6 +439,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
/>
)}
</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);
@@ -547,6 +603,58 @@ function _jumpToMeasurement(
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
const { id, sourceViewports, targetViewports } = synchronizerObj;
const synchronizer = syncGroupService.getSynchronizer(id);
if (!synchronizer) {
return;
}
const sourceViewportInfo = sourceViewports.find(
sourceViewport => sourceViewport.viewportId === viewportId
);
const targetViewportInfo = targetViewports.find(
targetViewport => targetViewport.viewportId === viewportId
);
const isSourceViewportInSynchronizer = synchronizer
.getSourceViewports()
.find(sourceViewport => sourceViewport.viewportId === viewportId);
const isTargetViewportInSynchronizer = synchronizer
.getTargetViewports()
.find(targetViewport => targetViewport.viewportId === viewportId);
// if the viewport was previously a source viewport, add it again
if (sourceViewportInfo && !isSourceViewportInSynchronizer) {
synchronizer.addSource({
viewportId: sourceViewportInfo.viewportId,
renderingEngineId: sourceViewportInfo.renderingEngineId,
});
}
// if the viewport was previously a target viewport, add it again
if (targetViewportInfo && !isTargetViewportInSynchronizer) {
synchronizer.addTarget({
viewportId: targetViewportInfo.viewportId,
renderingEngineId: targetViewportInfo.renderingEngineId,
});
}
});
}
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
@@ -2,21 +2,25 @@ 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 { ViewportOverlay } from '@ohif/ui';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { InstanceMetadata } from 'platform/core/src/types';
import { ServicesManager } from '@ohif/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
import { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css';
const EPSILON = 1e-4;
type ViewportData = StackViewportData | VolumeViewportData;
interface OverlayItemProps {
element: any;
viewportData: any;
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
servicesManager: ServicesManager;
viewportId: string;
instance: InstanceMetadata;
customization: any;
formatters: {
@@ -35,67 +39,11 @@ interface OverlayItemProps {
scale?: number;
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">I:</span>
<span className="font-light">
{instanceNumber !== undefined && instanceNumber !== null
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}
const OverlayItemComponents = {
'ohif.overlayItem.windowLevel': VOIOverlayItem,
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
/**
* Customizable Viewport Overlay
@@ -106,12 +54,17 @@ function CustomizableViewportOverlay({
imageSliceData,
viewportId,
servicesManager,
}: {
element: HTMLElement;
viewportData: ViewportData;
imageSliceData: ImageSliceData;
viewportId: string;
servicesManager: ServicesManager;
}) {
const { toolbarService, cornerstoneViewportService, customizationService } =
const { cornerstoneViewportService, customizationService, toolGroupService } =
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
const { imageIndex } = imageSliceData;
const topLeftCustomization = customizationService.getModeCustomization(
@@ -127,27 +80,21 @@ function CustomizableViewportOverlay({
'cornerstoneOverlayBottomRight'
);
const instance = useMemo(() => {
const instances = useMemo(() => {
if (viewportData != null) {
return _getViewportInstance(viewportData, imageIndex);
return _getViewportInstances(viewportData);
} else {
return null;
}
}, [viewportData, imageIndex]);
const instanceNumber = useMemo(() => {
if (viewportData != null) {
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
}
return null;
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
const instanceNumber = useMemo(
() =>
viewportData
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
: null,
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
);
/**
* Updating the VOI when the viewport changes its voi
@@ -215,26 +162,9 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, cornerstoneViewportService, element]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [toolbarService]);
const _renderOverlayItem = useCallback(
item => {
const overlayItemProps: OverlayItemProps = {
const overlayItemProps = {
element,
viewportData,
imageSliceData,
@@ -242,24 +172,26 @@ function CustomizableViewportOverlay({
servicesManager,
customization: item,
formatters: {
formatPN: formatPN,
formatPN,
formatDate: formatDICOMDate,
formatTime: formatDICOMTime,
formatNumberPrecision: formatNumberPrecision,
formatNumberPrecision,
},
instance,
// calculated
instance: instances ? instances[item?.instanceIndex] : null,
voi,
scale,
instanceNumber,
};
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
return <VOIOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
return <ZoomOverlayItem {...overlayItemProps} />;
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
return <InstanceNumberOverlayItem {...overlayItemProps} />;
if (!item) {
return null;
}
const { customizationType } = item;
const OverlayItemComponent = OverlayItemComponents[customizationType];
if (OverlayItemComponent) {
return <OverlayItemComponent {...overlayItemProps} />;
} else {
const renderItem = customizationService.transform(item);
@@ -275,110 +207,155 @@ function CustomizableViewportOverlay({
viewportId,
servicesManager,
customizationService,
instance,
instances,
voi,
scale,
instanceNumber,
]
);
const getTopLeftContent = useCallback(() => {
const items = topLeftCustomization?.items || [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topLeftCustomization, _renderOverlayItem]);
const getContent = useCallback(
(customization, defaultItems, keyPrefix) => {
const items = customization?.items ?? defaultItems;
return (
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{item?.condition
? item.condition({
instance: instances ? instances[item?.instanceIndex] : null,
formatters: { formatDate: formatDICOMDate },
})
? _renderOverlayItem(item)
: null
: _renderOverlayItem(item)}
</div>
))}
</>
);
},
[_renderOverlayItem]
);
const getTopRightContent = useCallback(() => {
const items = topRightCustomization?.items || [
{
id: 'InstanceNmber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
];
return (
<>
{items.map((item, i) => (
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [topRightCustomization, _renderOverlayItem]);
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ instance }) => instance && instance.StudyDate,
contentF: ({ instance, formatters: { formatDate } }) => formatDate(instance.StudyDate),
};
const getBottomLeftContent = useCallback(() => {
const items = bottomLeftCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomLeftCustomization, _renderOverlayItem]);
const seriesDescriptionItem = {
id: 'SeriesDescription',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Series description',
attribute: 'SeriesDescription',
condition: ({ instance }) => {
return instance && instance.SeriesDescription;
},
};
const getBottomRightContent = useCallback(() => {
const items = bottomRightCustomization?.items || [];
return (
<>
{items.map((item, i) => (
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
))}
</>
);
}, [bottomRightCustomization, _renderOverlayItem]);
const topLeftItems = instances
? instances
.map((instance, index) => {
return [
{
...studyDateItem,
instanceIndex: index,
},
{
...seriesDescriptionItem,
instanceIndex: index,
},
];
})
.flat()
: [];
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
bottomLeft={getBottomLeftContent()}
bottomRight={getBottomRightContent()}
topLeft={
/**
* Inline default overlay items for a more standard expansion
*/
getContent(topLeftCustomization, [...topLeftItems], 'topLeftOverlayItem')
}
topRight={getContent(topRightCustomization, [], 'topRightOverlayItem')}
bottomLeft={getContent(
bottomLeftCustomization,
[
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
},
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: () => {
const activeToolName = toolGroupService.getActiveToolForViewport(viewportId);
return activeToolName === 'Zoom';
},
},
],
'bottomLeftOverlayItem'
)}
bottomRight={getContent(
bottomRightCustomization,
[
{
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
],
'bottomRightOverlayItem'
)}
/>
);
}
function _getViewportInstance(viewportData, imageIndex) {
let imageId = null;
function _getViewportInstances(viewportData) {
const imageIds = [];
if (viewportData.viewportType === Enums.ViewportType.STACK) {
imageId = viewportData.data.imageIds[imageIndex];
imageIds.push(viewportData.data.imageIds[0]);
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
const volumes = viewportData.data;
if (volumes && volumes.length == 1) {
const volume = volumes[0];
imageId = volume.imageIds[imageIndex];
}
volumes.forEach(volume => {
if (!volume?.imageIds) {
return;
}
imageIds.push(volume.imageIds[0]);
});
}
return imageId ? metaData.get('instance', imageId) || {} : {};
const instances = [];
imageIds.forEach(imageId => {
const instance = metaData.get('instance', imageId) || {};
instances.push(instance);
});
return instances;
}
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber && instanceNumber !== 0) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
switch (viewportData.viewportType) {
case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break;
case Enums.ViewportType.ORTHOGRAPHIC:
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService,
imageIndex
);
break;
}
return instanceNumber;
}
return instanceNumber ?? null;
};
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
@@ -403,15 +380,20 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
// 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
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
const volumes = viewportData.volumes;
function _getInstanceNumberFromVolume(
viewportData,
viewportId,
cornerstoneViewportService,
imageIndex
) {
const volumes = viewportData.data;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
if (!volumes) {
return;
}
const volume = volumes[0];
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@@ -442,6 +424,72 @@ function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewp
}
}
/**
* Window Level / Center Overlay item
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">W:</span>
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
<span className="mr-1 shrink-0">L:</span>
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
</div>
);
}
/**
* Zoom Level Overlay item
*/
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span>{scale.toFixed(2)}x</span>
</div>
);
}
/**
* Instance Number Overlay Item
*/
function InstanceNumberOverlayItem({
instanceNumber,
imageSliceData,
customization,
}: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData;
return (
<div
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span>
{instanceNumber !== undefined && instanceNumber !== null ? (
<>
<span className="mr-1 shrink-0">I:</span>
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
</>
) : (
`${imageIndex + 1}/${numberOfSlices}`
)}
</span>
</div>
);
}
CustomizableViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
@@ -108,17 +108,15 @@ function ViewportOrientationMarkers({
console.log('ViewportOrientationMarkers::No viewport');
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
return orientationMarkers.map((m, index) => (
<div
className={classNames(
'overlay-text',
`${m}-mid orientation-marker`,
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
'text-aqua-pale',
'text-[13px]',
'leading-5'
)}
key={`${m}-mid orientation-marker`}
>
@@ -135,7 +133,7 @@ function ViewportOrientationMarkers({
element,
]);
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
}
ViewportOrientationMarkers.propTypes = {
@@ -1,278 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ViewportOverlay } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
const EPSILON = 1e-4;
function CornerstoneViewportOverlay({
element,
viewportData,
imageSliceData,
viewportId,
servicesManager,
}) {
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [activeTools, setActiveTools] = useState([]);
/**
* Initial toolbar state
*/
useEffect(() => {
setActiveTools(toolbarService.getActiveTools());
}, []);
useEffect(() => {
let isMounted = true;
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
if (!isMounted) {
return;
}
setActiveTools(toolbarService.getActiveTools());
}
);
return () => {
isMounted = false;
unsubscribe();
};
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportId, viewportData, voi, element]);
/**
* Updating the scale when the viewport changes its zoom
*/
useEffect(() => {
const updateScale = eventDetail => {
const { previousCamera, camera } = eventDetail.detail;
if (
previousCamera.parallelScale !== camera.parallelScale ||
previousCamera.scale !== camera.scale
) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const imageData = viewport.getImageData();
if (!imageData) {
return;
}
if (camera.scale) {
setScale(camera.scale);
return;
}
const { spacing } = imageData;
// convert parallel scale to scale
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
setScale(scale);
}
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
};
}, [viewportId, viewportData]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
if (activeTools.includes('WindowLevel')) {
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null;
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
);
}
if (activeTools.includes('Zoom')) {
return (
<div className="flex flex-row text-base">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
);
}
return null;
}, [voi, scale, activeTools]);
const getTopRightContent = useCallback(() => {
const { imageIndex, numberOfSlices } = imageSliceData;
if (!viewportData) {
return;
}
let instanceNumber;
if (viewportData.viewportType === Enums.ViewportType.STACK) {
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
if (!instanceNumber) {
return null;
}
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
instanceNumber = _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
);
}
return (
<div className="flex flex-row text-base">
<span className="mr-1">I:</span>
<span className="font-light">
{instanceNumber !== undefined
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
: `${imageIndex + 1}/${numberOfSlices}`}
</span>
</div>
);
}, [imageSliceData, viewportData, viewportId]);
if (!viewportData) {
return null;
}
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
// background
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
topRight={getTopRightContent()}
color={isLight && 'text-[#0944B3]'}
/>
);
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
return;
}
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const stackSize = imageIds.length;
if (stackSize <= 1) {
return;
}
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
function _getInstanceNumberFromVolume(
viewportData,
imageIndex,
viewportId,
cornerstoneViewportService
) {
const volumes = viewportData.volumes;
// Todo: support fusion of acquisition plane which has instanceNumber
if (!volumes || volumes.length > 1) {
return;
}
const volume = volumes[0];
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!cornerstoneViewport) {
return;
}
const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9);
// check if viewPlaneNormal is parallel to scanAxisNormal
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
if (isAcquisitionPlane) {
const imageId = imageIds[imageIndex];
if (!imageId) {
return {};
}
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
return parseInt(instanceNumber);
}
}
CornerstoneViewportOverlay.propTypes = {
viewportData: PropTypes.object,
imageIndex: PropTypes.number,
viewportId: PropTypes.string,
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default CornerstoneViewportOverlay;
+308 -124
View File
@@ -16,11 +16,17 @@ import { Types as OhifTypes } from '@ohif/core';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
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 { CornerstoneServices } from './types';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
voi: toggleVOISliceSync,
};
function commandsModule({
servicesManager,
@@ -30,11 +36,14 @@ function commandsModule({
viewportGridService,
toolGroupService,
cineService,
toolbarService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
measurementService,
customizationService,
colorbarService,
hangingProtocolService,
syncGroupService,
} = servicesManager.services as CornerstoneServices;
const { measurementServiceSource } = this;
@@ -132,23 +141,18 @@ function commandsModule({
* on the measurement with a response if not cancelled.
*/
setMeasurementLabel: ({ uid }) => {
const labelConfig = customizationService.get('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
callInputDialog(
uiDialogService,
measurement,
(label, actionId) => {
if (actionId === 'cancel') {
return;
}
const updatedMeasurement = Object.assign({}, measurement, {
label,
});
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
},
false
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(
(val: Map<any, any>) => {
measurementService.update(
uid,
{
...val,
},
true
);
}
);
},
@@ -224,47 +228,22 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: ({ callback, data }) => {
callInputDialog(uiDialogService, data, callback);
},
cleanUpCrosshairs: () => {
// if the crosshairs tool is active, deactivate it and set window level active
// since we are going back to main non-mpr HP
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (activeViewportToolGroup._toolInstances?.Crosshairs?.mode === Enums.ToolModes.Active) {
actions.toolbarServiceRecordInteraction({
interactionType: 'tool',
commands: [
{
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
arrowTextCallback: ({ callback, data, uid }) => {
const labelConfig = customizationService.get('measurementLabels');
callLabelAutocompleteDialog(uiDialogService, callback, {}, labelConfig);
},
toggleCine: () => {
const { viewports } = viewportGridService.getState();
const { isCineEnabled } = cineService.getState();
cineService.setIsCineEnabled(!isCineEnabled);
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
},
setWindowLevel({ window, level, toolGroupId }) {
setViewportWindowLevel({ viewportId, window, level }) {
// convert to numbers
const windowWidthNum = Number(window);
const windowCenterNum = Number(level);
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId);
@@ -280,34 +259,60 @@ function commandsModule({
viewport.render();
},
// Just call the toolbar service record interaction - allows
// executing a toolbar command as a full toolbar command with side affects
// coming from the ToolbarService itself.
toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props);
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
const hasColorbar = colorbarService.hasColorbar(viewportId);
if (hasColorbar) {
colorbarService.removeColorbar(viewportId);
return;
}
colorbarService.addColorbar(viewportId, displaySetInstanceUIDs, options);
},
// Enable or disable a toggleable command, without calling the activation
// Used to setup already active tools from hanging protocols
setToolbarToggled: props => {
toolbarService.setToggled(props.toolId, props.isActive ?? true);
},
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
uiNotificationService.show({
title: 'Crosshairs',
message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
type: 'info',
duration: 3000,
});
setWindowLevel(props) {
const { toolGroupId } = props;
const { viewportId } = _getActiveViewportEnabledElement();
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
throw new Error('Crosshairs tool is not available in this viewport');
}
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
return;
}
actions.setViewportWindowLevel({ ...props, viewportId });
},
setToolEnabled: ({ toolName, toggle }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
return;
}
const toolGroup = toolGroupService.getToolGroup(null);
if (!toolGroup) {
return;
}
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
// Toggle the tool's state only if the toggle is true
if (toggle) {
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
}
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
const toolName = itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
actions.setToolActive({ toolName, toolGroupId });
});
},
setToolActive: ({ toolName, toolGroupId = null }) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
@@ -320,34 +325,17 @@ function commandsModule({
return;
}
if (!toolGroup.getToolInstance(toolName)) {
uiNotificationService.show({
title: `${toolName} tool`,
message: `The ${toolName} tool is not available in this viewport.`,
type: 'info',
duration: 3000,
});
throw new Error(`ToolGroup ${toolGroup.id} does not have this tool.`);
if (!toolGroup.hasTool(toolName)) {
return;
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
if (activeToolName) {
// Todo: this is a hack to prevent the crosshairs to stick around
// after another tool is selected. We should find a better way to do this
if (activeToolName === 'Crosshairs') {
toolGroup.setToolDisabled(activeToolName);
} else {
toolGroup.setToolPassive(activeToolName);
}
}
// If there is a toggle state, then simply set the enabled/disabled state without
// setting the tool active.
if (toggledState != null) {
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
return;
const activeToolOptions = toolGroup.getToolConfiguration(activeToolName);
activeToolOptions?.disableOnPassive
? toolGroup.setToolDisabled(activeToolName)
: toolGroup.setToolPassive(activeToolName);
}
// Set the new toolName to be active
@@ -540,18 +528,51 @@ function commandsModule({
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
setViewportColormap: ({
viewportId,
displaySetInstanceUID,
colormap,
opacity = 1,
immediate = false,
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
// Get display set options for the specified viewport ID
const displaySetsInfo = viewportMatchDetails.get(viewportId)?.displaySetsInfo;
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid.includes(displaySetInstanceUID);
});
if (displaySetsInfo) {
// Find the display set that matches the given UID
const matchingDisplaySet = displaySetsInfo.find(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
// If a matching display set is found, update the opacity with its value
hpOpacity = matchingDisplaySet?.displaySetOptions?.options?.colormap?.opacity;
}
const { actor: volumeActor, uid: volumeId } = actorEntry;
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
viewport.setProperties({ colormap, volumeActor }, volumeId);
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);
}
if (viewport instanceof VolumeViewport) {
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
viewport.render();
@@ -565,33 +586,59 @@ function commandsModule({
(currentIndex + direction + viewportIds.length) % viewportIds.length;
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
},
/**
* If the syncId is given and a synchronizer with that ID already exists, it will
* toggle it on/off for the provided viewports. If not, it will attempt to create
* a new synchronizer using the given syncId and type for the specified viewports.
* If no viewports are provided, you may notice some default behavior.
* - 'voi' type, we will aim to synchronize all viewports with the same modality
* -'imageSlice' type, we will aim to synchronize all viewports with the same orientation.
*
* @param options
* @param options.viewports - The viewports to synchronize
* @param options.syncId - The synchronization group ID
* @param options.type - The type of synchronization to perform
*/
toggleSynchronizer: ({ type, viewports, syncId }) => {
const synchronizer = syncGroupService.getSynchronizer(syncId);
toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({
servicesManager,
toggledState,
});
if (synchronizer) {
synchronizer.isDisabled() ? synchronizer.setEnabled(true) : synchronizer.setEnabled(false);
return;
}
const fn = toggleSyncFunctions[type];
if (fn) {
fn({
servicesManager,
viewports,
syncId,
});
}
},
setSourceViewportForReferenceLinesTool: ({ toggledState, viewportId }) => {
setSourceViewportForReferenceLinesTool: ({ viewportId }) => {
if (!viewportId) {
const { activeViewportId } = viewportGridService.getState();
viewportId = activeViewportId;
viewportId = activeViewportId ?? 'default';
}
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
toolGroup.setToolConfiguration(
toolGroup?.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
storePresentation: ({ viewportId }) => {
cornerstoneViewportService.storePresentation({ viewportId });
},
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const command = protocol.callbacks.onViewportDataInitialized;
@@ -608,6 +655,127 @@ function commandsModule({
}
});
},
setViewportPreset: ({ viewportId, preset }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
viewport.setProperties({
preset,
});
viewport.render();
},
/**
* Sets the volume quality for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the volume quality.
* @param {number} volumeQuality - The desired quality level of the volume rendering.
*/
setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const dims = image.getDimensions();
const spacing = image.getSpacing();
const spatialDiagonal = vec3.length(
vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2])
);
let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;
sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0;
const samplesPerRay = spatialDiagonal / sampleDistance + 1;
mapper.setMaximumSamplesPerRay(samplesPerRay);
mapper.setSampleDistance(sampleDistance);
viewport.render();
},
/**
* Shifts opacity points for a given viewport id.
* @param {string} viewportId - The ID of the viewport to set the mapping range.
* @param {number} shift - The shift value to shift the points by.
*/
shiftVolumeOpacityPoints: ({ viewportId, shift }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
const opacityPointValues = []; // Array to hold values
// Gather Existing Values
const size = ofun.getSize();
for (let pointIdx = 0; pointIdx < size; pointIdx++) {
const opacityPointValue = [0, 0, 0, 0];
ofun.getNodeValue(pointIdx, opacityPointValue);
// opacityPointValue now holds [xLocation, opacity, midpoint, sharpness]
opacityPointValues.push(opacityPointValue);
}
// Add offset
opacityPointValues.forEach(opacityPointValue => {
opacityPointValue[0] += shift; // Change the location value
});
// Set new values
ofun.removeAllPoints();
opacityPointValues.forEach(opacityPointValue => {
ofun.addPoint(...opacityPointValue);
});
viewport.render();
},
/**
* Sets the volume lighting settings for a given viewport.
* @param {string} viewportId - The ID of the viewport to set the lighting settings.
* @param {Object} options - The lighting settings to be set.
* @param {boolean} options.shade - The shade setting for the lighting.
* @param {number} options.ambient - The ambient setting for the lighting.
* @param {number} options.diffuse - The diffuse setting for the lighting.
* @param {number} options.specular - The specular setting for the lighting.
**/
setVolumeLighting: ({ viewportId, options }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const property = actor.getProperty();
if (options.shade !== undefined) {
property.setShade(options.shade);
}
if (options.ambient !== undefined) {
property.setAmbient(options.ambient);
}
if (options.diffuse !== undefined) {
property.setDiffuse(options.diffuse);
}
if (options.specular !== undefined) {
property.setSpecular(options.specular);
}
viewport.render();
},
resetCrosshairs: ({ viewportId }) => {
const crosshairInstances = [];
const getCrosshairInstances = toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
};
if (!viewportId) {
const toolGroupIds = toolGroupService.getToolGroupIds();
toolGroupIds.forEach(getCrosshairInstances);
} else {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
getCrosshairInstances(toolGroup.id);
}
crosshairInstances.forEach(ins => {
ins.resetCrosshairs();
});
},
};
const definitions = {
@@ -615,7 +783,6 @@ function commandsModule({
// context menu
showCornerstoneContextMenu: {
commandFn: actions.showCornerstoneContextMenu,
storeContexts: [],
options: {
menuCustomizationId: 'measurementsContextMenu',
commands: [
@@ -634,6 +801,9 @@ function commandsModule({
storeContexts: [],
options: {},
},
toggleViewportColorbar: {
commandFn: actions.toggleViewportColorbar,
},
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
},
@@ -643,16 +813,21 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
setViewportWindowLevel: {
commandFn: actions.setViewportWindowLevel,
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
},
setToolActive: {
commandFn: actions.setToolActive,
},
setToolActiveToolbar: {
commandFn: actions.setToolActiveToolbar,
},
setToolEnabled: {
commandFn: actions.setToolEnabled,
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
options: { rotation: 90 },
@@ -726,24 +901,33 @@ function commandsModule({
setViewportColormap: {
commandFn: actions.setViewportColormap,
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
},
setSourceViewportForReferenceLinesTool: {
commandFn: actions.setSourceViewportForReferenceLinesTool,
},
storePresentation: {
commandFn: actions.storePresentation,
},
setToolbarToggled: {
commandFn: actions.setToolbarToggled,
},
cleanUpCrosshairs: {
commandFn: actions.cleanUpCrosshairs,
},
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
setViewportPreset: {
commandFn: actions.setViewportPreset,
},
setVolumeRenderingQulaity: {
commandFn: actions.setVolumeRenderingQulaity,
},
shiftVolumeOpacityPoints: {
commandFn: actions.shiftVolumeOpacityPoints,
},
setVolumeLighting: {
commandFn: actions.setVolumeLighting,
},
resetCrosshairs: {
commandFn: actions.resetCrosshairs,
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
},
};
return {
@@ -1,38 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useAppConfig } from '@state';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const {
toolbarService,
customizationService,
displaySetService,
viewportGridService,
cineService,
} = servicesManager.services;
const [{ isCineEnabled, cines }] = useCine();
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const [{ isCineEnabled, cines }, api] = useCine();
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
const [appConfig] = useAppConfig();
const isMountedRef = useRef(null);
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const handleCineClose = () => {
toolbarService.recordInteraction({
groupId: 'MoreTools',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleCine',
commandOptions: {},
toolName: 'cine',
context: 'CORNERSTONE',
},
],
});
};
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
return;
@@ -45,11 +25,11 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(enabledVPElement, {
api.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(enabledVPElement);
api.stopClip(enabledVPElement);
}
};
@@ -70,34 +50,36 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
});
if (isPlaying) {
cineService.setIsCineEnabled(isPlaying);
api.setIsCineEnabled(isPlaying);
}
cineService.setCine({ id: viewportId, isPlaying, frameRate });
api.setCine({ id: viewportId, isPlaying, frameRate });
setNewStackFrameRate(frameRate);
}, [cineService, displaySetService, viewportId, viewportGridService, cines]);
}, [displaySetService, viewportId, viewportGridService, cines]);
useEffect(() => {
isMountedRef.current = true;
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
isMountedRef.current = false;
api.stopClip(enabledVPElement);
api.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
};
}, [enabledVPElement, newStackCineHandler]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
return;
}
cineHandler();
return () => {
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
cineService.stopClip(enabledVPElement);
}
api.stopClip(enabledVPElement);
};
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
}, [cines, viewportId, enabledVPElement, cineHandler]);
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
@@ -108,15 +90,22 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={handleCineClose}
onPlayPauseChange={isPlaying =>
cineService.setCine({
onClose={() => {
// also stop the clip
api.setCine({
id: viewportId,
isPlaying: false,
});
api.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
api.setCine({
id: viewportId,
isPlaying,
})
}
});
}}
onFrameRateChange={frameRate =>
cineService.setCine({
api.setCine({
id: viewportId,
frameRate,
})
@@ -0,0 +1,23 @@
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { ViewportActionCorners } from '@ohif/ui';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const [viewportActionCornersState] = useViewportActionCornersContext();
if (!viewportActionCornersState[viewportId]) {
return null;
}
return (
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
);
}
export default OHIFViewportActionCorners;
@@ -0,0 +1,114 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { StackViewport, VolumeViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
export function setViewportColorbar(
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarOptions
) {
const { cornerstoneViewportService } = serviceManager.services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '12px Arial',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
const displaySetInstanceUIDs = [];
if (viewport instanceof StackViewport) {
displaySetInstanceUIDs.push(viewportId);
}
if (viewport instanceof VolumeViewport) {
displaySets.forEach(ds => {
displaySetInstanceUIDs.push(ds.displaySetInstanceUID);
});
}
commandsManager.run({
commandName: 'toggleViewportColorbar',
commandOptions: {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
},
context: 'CORNERSTONE',
});
}
export function Colorbar({
viewportId,
displaySets,
commandsManager,
serviceManager,
colorbarProperties,
}: ColorbarProps): ReactElement {
const { colorbarService } = serviceManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
return (
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Display Color bar"
checked={showColorbar}
onChange={() => {
onSetColorbar();
}}
/>
</div>
);
}
@@ -0,0 +1,153 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
colormaps,
viewportId,
displaySets,
commandsManager,
serviceManager,
}: ColormapProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const activeDisplaySetRef = useRef(activeDisplaySet);
activeDisplaySetRef.current = activeDisplaySet;
const onSetColorLUT = useCallback(
props => {
// TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
...props,
opacity,
immediate: true,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
const getViewportColormap = (viewportId, displaySet) => {
const { displaySetInstanceUID } = displaySet;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const { colormap } = viewport.getProperties(actorEntry.uid);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
};
const buttons = useMemo(() => {
return displaySets.map((displaySet, index) => ({
children: displaySet.Modality,
key: index,
style: {
minWidth: `calc(100% / ${displaySets.length})`,
},
}));
}, [displaySets]);
useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
return (
<>
{buttons.length > 1 && (
<div className="all-in-one-menu-item flex w-full justify-center">
<ButtonGroup
onActiveIndexChange={index => {
setActiveDisplaySet(displaySets[index]);
setPrePreviewColormap(null);
}}
activeIndex={
displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
) || 1
}
className="w-[70%] text-[10px]"
>
{buttons.map(({ children, key, style }) => (
<div
key={key}
style={style}
>
{children}
</div>
))}
</ButtonGroup>
</div>
)}
<div className="all-in-one-menu-item flex w-full justify-center">
<SwitchButton
label="Preview in viewport"
checked={showPreview}
onChange={checked => {
setShowPreview(checked);
}}
/>
</div>
<AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
onClick={() => {
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
onSetColorLUT({
viewportId,
colormap: prePreviewColormapRef.current,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
</>
);
}
@@ -0,0 +1,118 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import { VolumeLightingProps } from '../../types/ViewportPresets';
export function VolumeLighting({
serviceManager,
commandsManager,
viewportId,
}: VolumeLightingProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [ambient, setAmbient] = useState(null);
const [diffuse, setDiffuse] = useState(null);
const [specular, setSpecular] = useState(null);
const onAmbientChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { ambient } });
}, [ambient, commandsManager, viewportId]);
const onDiffuseChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { diffuse } });
}, [diffuse, commandsManager, viewportId]);
const onSpecularChange = useCallback(() => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { specular } });
}, [specular, commandsManager, viewportId]);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ambient = actor.getProperty().getAmbient();
const diffuse = actor.getProperty().getDiffuse();
const specular = actor.getProperty().getSpecular();
setAmbient(ambient);
setDiffuse(diffuse);
setSpecular(specular);
}, [viewportId, cornerstoneViewportService]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="ambient"
>
Ambient
</label>
{ambient !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={ambient}
onChange={e => {
setAmbient(e.target.value);
onAmbientChange();
}}
id="ambient"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(ambient) }}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="diffuse"
>
Diffuse
</label>
{diffuse !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={diffuse}
onChange={e => {
setDiffuse(e.target.value);
onDiffuseChange();
}}
id="diffuse"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(diffuse) }}
/>
)}
</div>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="specular"
>
Specular
</label>
{specular !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={specular}
onChange={e => {
setSpecular(e.target.value);
onSpecularChange();
}}
id="specular"
max={1}
min={0}
type="range"
step={0.1}
style={{ background: calculateBackground(specular) }}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,46 @@
import React, { ReactElement } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
export function VolumeRenderingOptions({
viewportId,
commandsManager,
volumeRenderingQualityRange,
serviceManager,
}: VolumeRenderingOptionsProps): ReactElement {
return (
<AllInOneMenu.ItemPanel>
<VolumeRenderingQuality
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeShift
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
<div className="all-in-one-menu-item flex w-full justify-start">
<div className="text-aqua-pale text-[13px]">LIGHTING</div>
</div>
<AllInOneMenu.DividerItem />
<div className="all-in-one-menu-item flex w-full justify-center">
<VolumeShade
commandsManager={commandsManager}
serviceManager={serviceManager}
viewportId={viewportId}
/>
</div>
<VolumeLighting
viewportId={viewportId}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.ItemPanel>
);
}
@@ -0,0 +1,38 @@
import { AllInOneMenu, Icon } from '@ohif/ui';
import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
export function VolumeRenderingPresets({
viewportId,
serviceManager,
commandsManager,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
const { uiModalService } = serviceManager.services;
const onClickPresets = () => {
uiModalService.show({
content: VolumeRenderingPresetsContent,
title: 'Rendering Presets',
movable: true,
contentProps: {
onClose: uiModalService.hide,
presets: volumeRenderingPresets,
viewportId,
commandsManager,
},
containerDimensions: 'h-[543px] w-[460px]',
contentDimensions: 'h-[493px] w-[460px] pl-[12px]',
});
};
return (
<AllInOneMenu.Item
label="Rendering Presets"
icon={<Icon name="VolumeRendering" />}
rightIcon={<Icon name="action-new-dialog" />}
onClick={onClickPresets}
/>
);
}
@@ -0,0 +1,95 @@
import { Icon } from '@ohif/ui';
import { ButtonEnums } from '@ohif/ui';
import React, { ReactElement, useState, useCallback } from 'react';
import { Button, InputFilterText } from '@ohif/ui';
import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets';
export function VolumeRenderingPresetsContent({
presets,
viewportId,
commandsManager,
onClose,
}: VolumeRenderingPresetsContentProps): ReactElement {
const [filteredPresets, setFilteredPresets] = useState(presets);
const [searchValue, setSearchValue] = useState('');
const [selectedPreset, setSelectedPreset] = useState<ViewportPreset | null>(null);
const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
const filtered = value
? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase()))
: presets;
setFilteredPresets(filtered);
},
[presets]
);
const handleApply = useCallback(
props => {
commandsManager.runCommand('setViewportPreset', {
...props,
});
},
[commandsManager]
);
const formatLabel = (label: string, maxChars: number) => {
return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label;
};
return (
<div className="flex min-h-full w-full flex-col justify-between">
<div className="border-secondary-light h-[433px] w-full overflow-hidden rounded border bg-black px-2.5">
<div className="flex h-[46px] w-full items-center justify-start">
<div className="h-[26px] w-[200px]">
<InputFilterText
value={searchValue}
onDebounceChange={handleSearchChange}
placeholder={'Search all'}
/>
</div>
</div>
<div className="ohif-scrollbar overflow h-[385px] w-full overflow-y-auto">
<div className="grid grid-cols-4 gap-3 pt-2 pr-3">
{filteredPresets.map((preset, index) => (
<div
key={index}
className="flex cursor-pointer flex-col items-start"
onClick={() => {
setSelectedPreset(preset);
handleApply({ preset: preset.name, viewportId });
}}
>
<Icon
name={preset.name}
className={
selectedPreset?.name === preset.name
? 'border-primary-light h-[75px] w-[95px] max-w-none rounded border-2'
: 'hover:border-primary-light h-[75px] w-[95px] max-w-none rounded border-2 border-black'
}
/>
<label className="text-aqua-pale mt-2 text-left text-xs">
{formatLabel(preset.name, 11)}
</label>
</div>
))}
</div>
</div>
</div>
<footer className="flex h-[60px] w-full items-center justify-end">
<div className="flex">
<Button
name="Cancel"
size={ButtonEnums.size.medium}
type={ButtonEnums.type.secondary}
onClick={onClose}
>
{' '}
Cancel{' '}
</Button>
</div>
</footer>
</div>
);
}
@@ -0,0 +1,69 @@
import React, { ReactElement, useCallback, useState, useEffect } from 'react';
import { VolumeRenderingQualityProps } from '../../types/ViewportPresets';
export function VolumeRenderingQuality({
volumeRenderingQualityRange,
commandsManager,
serviceManager,
viewportId,
}: VolumeRenderingQualityProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const { min, max, step } = volumeRenderingQualityRange;
const [quality, setQuality] = useState(null);
const onChange = useCallback(
(value: number) => {
commandsManager.runCommand('setVolumeRenderingQulaity', {
viewportId,
volumeQuality: value,
});
setQuality(value);
},
[commandsManager, viewportId]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const mapper = actor.getMapper();
const image = mapper.getInputData();
const spacing = image.getSpacing();
const sampleDistance = mapper.getSampleDistance();
const averageSpacing = spacing.reduce((a, b) => a + b) / 3.0;
if (sampleDistance === averageSpacing) {
setQuality(1);
} else {
setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5)));
}
}, [cornerstoneViewportService, viewportId]);
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="volume"
>
Quality
</label>
{quality !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={quality}
id="volume"
max={max}
min={min}
type="range"
step={step}
onChange={e => onChange(parseInt(e.target.value, 10))}
style={{ background: calculateBackground((quality - min) / (max - min)) }}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,39 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { SwitchButton } from '@ohif/ui';
import { VolumeShadeProps } from '../../types/ViewportPresets';
export function VolumeShade({
commandsManager,
viewportId,
serviceManager,
}: VolumeShadeProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [shade, setShade] = useState(true);
const [key, setKey] = useState(0);
const onShadeChange = useCallback(
(checked: boolean) => {
commandsManager.runCommand('setVolumeLighting', { viewportId, options: { shade: checked } });
},
[commandsManager, viewportId]
);
useEffect(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const shade = actor.getProperty().getShade();
setShade(shade);
setKey(key + 1);
}, [viewportId, cornerstoneViewportService]);
return (
<SwitchButton
key={key}
label="Shade"
checked={shade}
onChange={() => {
setShade(!shade);
onShadeChange(!shade);
}}
/>
);
}
@@ -0,0 +1,91 @@
import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react';
import { VolumeShiftProps } from '../../types/ViewportPresets';
export function VolumeShift({
viewportId,
commandsManager,
serviceManager,
}: VolumeShiftProps): ReactElement {
const { cornerstoneViewportService } = serviceManager.services;
const [minShift, setMinShift] = useState<number | null>(null);
const [maxShift, setMaxShift] = useState<number | null>(null);
const [shift, setShift] = useState<number | null>(
cornerstoneViewportService.getCornerstoneViewport(viewportId)?.shiftedBy || 0
);
const [step, setStep] = useState<number | null>(null);
const [isBlocking, setIsBlocking] = useState(false);
const prevShiftRef = useRef<number>(shift);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0];
const ofun = actor.getProperty().getScalarOpacity(0);
useEffect(() => {
if (isBlocking) {
return;
}
const range = ofun.getRange();
const transferFunctionWidth = range[1] - range[0];
const minShift = -transferFunctionWidth;
const maxShift = transferFunctionWidth;
setMinShift(minShift);
setMaxShift(maxShift);
setStep(Math.pow(10, Math.floor(Math.log10(transferFunctionWidth / 500))));
}, [cornerstoneViewportService, viewportId, actor, ofun, isBlocking]);
const onChangeRange = useCallback(
newShift => {
const shiftDifference = newShift - prevShiftRef.current;
prevShiftRef.current = newShift;
viewport.shiftedBy = newShift;
commandsManager.runCommand('shiftVolumeOpacityPoints', {
viewportId,
shift: shiftDifference,
});
},
[commandsManager, viewportId, viewport]
);
const calculateBackground = value => {
const percentage = ((value - 0) / (1 - 0)) * 100;
return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`;
};
return (
<>
<div className="all-in-one-menu-item flex w-full flex-row !items-center justify-between gap-[10px]">
<label
className="block text-white"
htmlFor="shift"
>
Shift
</label>
{step !== null && (
<input
className="bg-inputfield-main h-2 w-[120px] cursor-pointer appearance-none rounded-lg"
value={shift}
onChange={e => {
const shiftValue = parseInt(e.target.value, 10);
setShift(shiftValue);
onChangeRange(shiftValue);
}}
id="shift"
onMouseDown={() => setIsBlocking(true)}
onMouseUp={() => setIsBlocking(false)}
max={maxShift}
min={minShift}
type="range"
step={step}
style={{
background: calculateBackground((shift - minShift) / (maxShift - minShift)),
}}
/>
)}
</div>
</>
);
}
@@ -0,0 +1,48 @@
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { CommandsManager } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export type WindowLevelProps = {
viewportId: string;
presets: Record<string, Array<WindowLevelPreset>>;
commandsManager: CommandsManager;
};
export function WindowLevel({
viewportId,
commandsManager,
presets,
}: WindowLevelProps): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
const onSetWindowLevel = useCallback(
props => {
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...props,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
return (
<AllInOneMenu.ItemPanel>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality: Object.keys(presets)[0] })}
</AllInOneMenu.HeaderItem>
{Object.values(presets)[0].map((preset, index) => (
<AllInOneMenu.Item
key={index}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
onClick={() => onSetWindowLevel({ ...preset, viewportId })}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
);
}
@@ -0,0 +1,205 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { AllInOneMenu, useViewportGrid } from '@ohif/ui';
import { CommandsManager, ServicesManager } from '@ohif/core';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
import { setViewportColorbar } from './Colorbar';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { ColorbarProperties } from '../../types/Colorbar';
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Record<string, Array<WindowLevelPreset>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export function WindowLevelActionMenu({
viewportId,
element,
presets,
verticalDirection,
horizontalDirection,
commandsManager,
serviceManager,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: WindowLevelActionMenuProps): ReactElement {
const {
colormaps,
colorbarContainerPosition,
colorbarInitialColormap,
colorbarTickPosition,
width: colorbarWidth,
} = colorbarProperties;
const { colorbarService, cornerstoneViewportService } = serviceManager.services;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
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();
const { activeViewportId } = viewportGrid;
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
const [menuKey, setMenuKey] = useState(0);
const [is3DVolume, setIs3DVolume] = useState(false);
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, displaySets, commandsManager, serviceManager, {
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const newVpHeight = element?.clientHeight;
if (vpHeight !== newVpHeight) {
setVpHeight(newVpHeight);
}
}, [element, vpHeight]);
useEffect(() => {
if (!colorbarService.hasColorbar(viewportId)) {
return;
}
window.setTimeout(() => {
colorbarService.removeColorbar(viewportId);
onSetColorbar();
}, 0);
}, [viewportId]);
useEffect(() => {
if (colorbarService.hasColorbar(viewportId)) {
colorbarService.removeColorbar(viewportId);
}
}, [displaySets]);
useEffect(() => {
setMenuKey(menuKey + 1);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport3D) {
setIs3DVolume(true);
} else {
setIs3DVolume(false);
}
}, [
displaySets,
viewportId,
presets,
volumeRenderingQualityRange,
volumeRenderingPresets,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
return (
<AllInOneMenu.IconMenu
icon="viewport-window-level"
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
'flex shrink-0 cursor-pointer rounded active:text-white',
isLight
? 'text-aqua-pale hover:bg-secondary-dark'
: 'text-primary-light hover:bg-secondary-light/60'
)}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => {
setVpHeight(element.clientHeight);
}}
menuKey={menuKey}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
colorbarProperties={colorbarProperties}
/>
)}
{colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
>
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
commandsManager={commandsManager}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
{presets && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets', { modality: Object.keys(presets)[0] })}
itemIcon="viewport-window-level"
>
<WindowLevel
viewportId={viewportId}
commandsManager={commandsManager}
presets={presets}
/>
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
serviceManager={serviceManager}
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions
viewportId={viewportId}
commandsManager={commandsManager}
volumeRenderingQualityRange={volumeRenderingQualityRange}
serviceManager={serviceManager}
/>
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
</AllInOneMenu.IconMenu>
);
}
@@ -0,0 +1,23 @@
// The following are the default window level presets and can be further
// configured via the customization service.
const defaultWindowLevelPresets = {
CT: [
{ description: 'Soft tissue', window: '400', level: '40' },
{ description: 'Lung', window: '1500', level: '-600' },
{ description: 'Liver', window: '150', level: '90' },
{ description: 'Bone', window: '2500', level: '480' },
{ description: 'Brain', window: '80', level: '40' },
],
PT: [
{ description: 'Default', window: '5', level: '2.5' },
{ description: 'SUV', window: '0', level: '3' },
{ description: 'SUV', window: '0', level: '5' },
{ description: 'SUV', window: '0', level: '7' },
{ description: 'SUV', window: '0', level: '8' },
{ description: 'SUV', window: '0', level: '10' },
{ description: 'SUV', window: '0', level: '15' },
],
};
export default defaultWindowLevelPresets;
@@ -0,0 +1,44 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
element,
displaySets,
servicesManager,
commandsManager,
verticalDirection,
horizontalDirection,
}): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.get('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } = customizationService.get(
'cornerstone.3dVolumeRendering'
);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const hasMenu = displaySetPresets.length > 0;
return hasMenu ? (
<WindowLevelActionMenu
viewportId={viewportId}
element={element}
presets={displaySetPresets[0]}
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
commandsManager={commandsManager}
serviceManager={servicesManager}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
) : null;
}
@@ -0,0 +1,159 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
} from 'react';
import PropTypes from 'prop-types';
import { Types, ViewportActionCornersLocations } from '@ohif/ui';
import ViewportActionCornersService, {
ActionComponentInfo,
} from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface StateComponentInfo extends Types.ViewportActionCornersComponentInfo {
indexPriority: number;
}
type State = Record<string, Record<ViewportActionCornersLocations, Array<StateComponentInfo>>>;
const DEFAULT_STATE: State = {
// default here is the viewportId of the default viewport
default: {
[ViewportActionCornersLocations.topLeft]: [],
[ViewportActionCornersLocations.topRight]: [],
[ViewportActionCornersLocations.bottomLeft]: [],
[ViewportActionCornersLocations.bottomRight]: [],
},
// [anotherViewportId]: { ..... }
};
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;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
: [];
// If the component (id) already exists at the location specified in the payload,
// then it must be replaced with the component in the payload so first
// remove it from that location.
const deletionIndex = locationComponents.findIndex(component => component.id === id);
if (deletionIndex !== -1) {
locationComponents = [
...locationComponents.slice(0, deletionIndex),
...locationComponents.slice(deletionIndex + 1),
];
}
// 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
);
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
indexPriority,
},
...locationComponents.slice(insertionIndex + 1),
];
}
return {
...state,
...{
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
},
};
}
case 'CLEAR_ACTION_COMPONENTS': {
const viewportId = action.payload;
const nextState = { ...state };
delete nextState[viewportId];
return nextState;
}
default:
return { ...state };
}
};
const [viewportActionCornersState, dispatch] = useReducer(
viewportActionCornersReducer,
DEFAULT_STATE
);
const getState = useCallback(() => {
return viewportActionCornersState;
}, [viewportActionCornersState]);
const setComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
const setComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
);
const clear = useCallback(
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setComponent,
setComponents,
clear,
});
}
}, [getState, service, setComponent, setComponents]);
// run many of the calls through the service itself since we want to publish events
const api = {
getState,
setComponent: props => service.setComponent(props),
setComponents: props => service.setComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
() => [viewportActionCornersState, api],
[viewportActionCornersState, api]
);
return (
<ViewportActionCornersContext.Provider value={contextValue}>
{children}
</ViewportActionCornersContext.Provider>
);
}
ViewportActionCornersProvider.propTypes = {
children: PropTypes.node,
service: PropTypes.instanceOf(ViewportActionCornersService).isRequired,
};
export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext);
@@ -1,6 +1,12 @@
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';
const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS;
const tools = {
active: [
@@ -37,6 +43,27 @@ function getCustomizationModule() {
id: 'cornerstone.overlayViewportTools',
tools,
},
{
id: 'cornerstone.windowLevelPresets',
presets: defaultWindowLevelPresets,
},
{
id: 'cornerstone.colorbar',
width: '16px',
colorbarTickPosition: 'left',
colormaps,
colorbarContainerPosition: 'right',
colorbarInitialColormap: DefaultColormap,
},
{
id: 'cornerstone.3dVolumeRendering',
volumeRenderingPresets: VIEWPORT_PRESETS,
volumeRenderingQualityRange: {
min: 1,
max: 4,
step: 1,
},
},
],
},
];
@@ -1,295 +1,10 @@
import { Types } from '@ohif/core';
const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'Multi-Planar Reconstruction',
locked: true,
createdDate: '2021-02-23',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
callbacks: {
// Switches out of MPR mode when the layout change button is used
onLayoutChange: [
{
commandName: 'toggleHangingProtocol',
commandOptions: { protocolId: 'mpr' },
context: 'DEFAULT',
},
],
// Turns off crosshairs when switching out of MPR mode
onProtocolExit: [
{
commandName: 'cleanUpCrosshairs',
},
],
},
displaySetSelectors: {
activeDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
name: 'MPR 1x3',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
],
},
},
viewports: [
{
viewportOptions: {
viewportId: 'mpr-axial',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-sagittal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-coronal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
],
},
],
};
const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
],
},
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
import { fourUp } from './hps/fourUp';
import { main3D } from './hps/main3D';
import { mpr } from './hps/mpr';
import { mprAnd3DVolumeViewport } from './hps/mprAnd3DVolumeViewport';
import { only3D } from './hps/only3D';
import { primary3D } from './hps/primary3D';
import { primaryAxial } from './hps/primaryAxial';
function getHangingProtocolModule() {
return [
@@ -301,6 +16,26 @@ function getHangingProtocolModule() {
name: mprAnd3DVolumeViewport.id,
protocol: mprAnd3DVolumeViewport,
},
{
name: fourUp.id,
protocol: fourUp,
},
{
name: main3D.id,
protocol: main3D,
},
{
name: primaryAxial.id,
protocol: primaryAxial,
},
{
name: only3D.id,
protocol: only3D,
},
{
name: primary3D.id,
protocol: primary3D,
},
];
}
@@ -0,0 +1,288 @@
import { Enums } from '@cornerstonejs/tools';
const getToggledClassName = (isToggled: boolean) => {
return isToggled
? '!text-primary-active'
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
export default function getToolbarModule({ commandsManager, servicesManager }) {
const {
toolGroupService,
syncGroupService,
cornerstoneViewportService,
hangingProtocolService,
displaySetService,
viewportGridService,
} = servicesManager.services;
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
// Todo: isActive right now is used for nested buttons where the primary
// button needs to be fully rounded (vs partial rounded) when active
// otherwise it does not have any other use
isActive: isPrimaryActive,
};
},
},
{
name: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
evaluate: ({ viewportId, button, itemId }) => {
const { items } = button.props;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return {
primary: button.props.primary,
items,
};
}
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
// check if the active toolName is part of the items then we need
// to move it to the primary button
const activeToolIndex = items.findIndex(item => {
const toolName = getToolNameForButton(item);
return toolName === activeToolName;
});
// if there is an active tool in the items dropdown bound to the primary mouse/touch
// we should show that no matter what
if (activeToolIndex > -1) {
return {
primary: items[activeToolIndex],
items,
};
}
if (!itemId) {
return {
primary: button.props.primary,
items,
};
}
// other wise we can move the clicked tool to the primary button
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
return {
primary: clickedItemProps,
items,
};
},
},
{
name: 'evaluate.action',
evaluate: ({ viewportId, button }) => {
return {
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
},
},
{
name: 'evaluate.cornerstoneTool.toggle',
evaluate: ({ viewportId, button, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isOff = [Enums.ToolModes.Disabled, Enums.ToolModes.Passive].includes(
toolGroup.getToolOptions(toolName).mode
);
return {
className: getToggledClassName(!isOff),
};
},
},
{
name: 'evaluate.cornerstone.synchronizer',
evaluate: ({ viewportId, button }) => {
let synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
const synchronizerType = button?.commands?.[0]?.commandOptions?.type;
synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType);
if (!synchronizers?.length) {
return {
className: getToggledClassName(false),
};
}
// Todo: we need a better way to find the synchronizers based on their
// type, but for now we just check the first one and see if it is
// enabled
const synchronizer = synchronizers[0];
const isEnabled = synchronizer?._enabled;
return {
className: getToggledClassName(isEnabled),
};
},
},
{
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 }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.isDisabled) {
return;
}
const propId = button.id;
const properties = viewport.getProperties();
const camera = viewport.getCamera();
const prop = camera?.[propId] || properties?.[propId];
if (!prop) {
return {
disabled: false,
className: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
};
}
const isToggled = prop;
return {
className: getToggledClassName(isToggled),
};
},
},
{
name: 'evaluate.mpr',
evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const areReconstructable = displaySets.every(displaySet => {
return displaySet.isReconstructable;
});
if (!areReconstructable) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
const isMpr = protocol?.id === 'mpr';
return {
disabled: false,
className: getToggledClassName(isMpr),
};
},
},
];
}
function getToolNameForButton(button) {
const { props } = button;
const commands = props?.commands || button.commands;
const commandsArray = Array.isArray(commands) ? commands : [commands];
const firstCommand = commandsArray[0];
if (typeof firstCommand === 'string') {
// likely not a cornerstone tool
return null;
}
if ('commandOptions' in firstCommand) {
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
}
// use id as a fallback for toolName
return props?.id ?? button.id;
}
+141
View File
@@ -0,0 +1,141 @@
export const fourUp = {
id: 'fourUp',
locked: true,
name: '3D four up',
icon: 'layout-advanced-3d-four-up',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'fourUpStage',
name: 'fourUp',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
// ToDo: choose appropriate preset
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
+167
View File
@@ -0,0 +1,167 @@
export const main3D = {
id: 'main3D',
locked: true,
name: '3D main',
icon: 'layout-advanced-3d-main',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'main3DStage',
name: 'main3D',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1,
height: 1 / 2,
},
{
x: 0,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
{
x: 1 / 3,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
{
x: 2 / 3,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
],
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
// ToDo: choose appropriate preset
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
+159
View File
@@ -0,0 +1,159 @@
import { Types } from '@ohif/core';
export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'MPR',
locked: true,
icon: 'layout-advanced-mpr',
isPreset: true,
createdDate: '2021-02-23',
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
callbacks: {
// Switches out of MPR mode when the layout change button is used
onLayoutChange: [
{
commandName: 'toggleHangingProtocol',
commandOptions: { protocolId: 'mpr' },
context: 'DEFAULT',
},
],
// Turns off crosshairs when switching out of MPR mode
},
displaySetSelectors: {
activeDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
name: 'MPR 1x3',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
],
},
},
viewports: [
{
viewportOptions: {
viewportId: 'mpr-axial',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-sagittal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'mpr-coronal',
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'activeDisplaySet',
},
],
},
],
},
],
};
@@ -0,0 +1,147 @@
export const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
name: 'mpr',
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
],
},
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
+63
View File
@@ -0,0 +1,63 @@
export const only3D = {
id: 'only3D',
locked: true,
name: '3D only',
icon: 'layout-advanced-3d-only',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'only3DStage',
name: 'only3D',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
// ToDo: choose appropriate preset
displayPreset: 'CT-Bone',
},
},
],
},
],
},
],
};
+167
View File
@@ -0,0 +1,167 @@
export const primary3D = {
id: 'primary3D',
locked: true,
name: '3D primary',
icon: 'layout-advanced-3d-primary',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'primary3DStage',
name: 'primary3D',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 3,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 2 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1 / 3,
},
{
x: 2 / 3,
y: 1 / 3,
width: 1 / 3,
height: 1 / 3,
},
{
x: 2 / 3,
y: 2 / 3,
width: 1 / 3,
height: 1 / 3,
},
],
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'volume3d',
viewportType: 'volume3d',
orientation: 'coronal',
customViewportProps: {
hideOverlays: true,
},
},
displaySets: [
{
id: 'mprDisplaySet',
options: {
// ToDo: choose appropriate preset
displayPreset: 'CT-Bone',
},
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
@@ -0,0 +1,142 @@
export const primaryAxial = {
id: 'primaryAxial',
locked: true,
name: 'Axial Primary',
icon: 'layout-advanced-axial-primary',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
imageLoadStrategy: 'interleaveCenter',
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
},
stages: [
{
id: 'primaryAxialStage',
name: 'primaryAxial',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 2 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1 / 2,
},
{
x: 2 / 3,
y: 1 / 2,
width: 1 / 3,
height: 1 / 2,
},
],
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
+16 -2
View File
@@ -13,11 +13,13 @@ import init from './init';
import getCustomizationModule from './getCustomizationModule';
import getCommandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getToolbarModule from './getToolbarModule';
import ToolGroupService from './services/ToolGroupService';
import SyncGroupService from './services/SyncGroupService';
import SegmentationService from './services/SegmentationService';
import CornerstoneCacheService from './services/CornerstoneCacheService';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import ColorbarService from './services/ColorbarService';
import * as CornerstoneExtensionTypes from './types';
import { toolNames } from './initCornerstoneTools';
@@ -26,9 +28,12 @@ import dicomLoaderService from './utils/dicomLoaderService';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import { showLabelAnnotationPopup } from './utils/callInputDialog';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
@@ -68,16 +73,23 @@ const cornerstoneExtension: Types.Extensions.Extension = {
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
*/
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
const { servicesManager } = props;
const { servicesManager, serviceProvidersManager } = props;
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
servicesManager.registerService(ToolGroupService.REGISTRATION);
servicesManager.registerService(SyncGroupService.REGISTRATION);
servicesManager.registerService(SegmentationService.REGISTRATION);
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
servicesManager.registerService(ViewportActionCornersService.REGISTRATION);
servicesManager.registerService(ColorbarService.REGISTRATION);
serviceProvidersManager.registerProvider(
ViewportActionCornersService.REGISTRATION.name,
ViewportActionCornersProvider
);
return init.call(this, props);
},
getToolbarModule,
getHangingProtocolModule,
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
@@ -115,6 +127,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
showLabelAnnotationPopup
},
},
{
@@ -140,5 +153,6 @@ export {
CornerstoneExtensionTypes as Types,
toolNames,
getActiveViewportEnabledElement,
ImageOverlayViewerTool,
};
export default cornerstoneExtension;
+51 -73
View File
@@ -1,4 +1,4 @@
import OHIF, { Types } from '@ohif/core';
import OHIF, { Types, errorHandler } from '@ohif/core';
import React from 'react';
import * as cornerstone from '@cornerstonejs/core';
@@ -10,11 +10,11 @@ import {
metaData,
volumeLoader,
imageLoadPoolManager,
getEnabledElement,
Settings,
utilities as csUtilities,
Enums as csEnums,
} from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import initWADOImageLoader from './initWADOImageLoader';
@@ -29,6 +29,10 @@ import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import { CornerstoneServices } from './types';
import initViewTiming from './utils/initViewTiming';
import { utilities } from '@cornerstonejs/core';
import { colormaps } from './utils/colormaps';
const { registerColormap } = utilities.colormap;
// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
@@ -40,7 +44,6 @@ export default async function init({
servicesManager,
commandsManager,
extensionManager,
configuration,
appConfig,
}: Types.Extensions.ExtensionParams): Promise<void> {
// Note: this should run first before initializing the cornerstone
@@ -93,12 +96,26 @@ export default async function init({
cineService,
cornerstoneViewportService,
hangingProtocolService,
toolGroupService,
toolbarService,
viewportGridService,
stateSyncService,
segmentationService,
} = servicesManager.services as CornerstoneServices;
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
]);
toolbarService.registerEventForToolbarUpdate(eventTarget, [
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
]);
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
@@ -124,6 +141,9 @@ export default async function init({
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
// Stores synchronizers state to be restored
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
// Stores a map from `positionPresentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same position
stateSyncService.register('positionPresentationStore', {
@@ -199,6 +219,16 @@ export default async function init({
}
);
// resize the cornerstone viewport service when the grid size changes
// IMPORTANT: this should happen outside of the OHIFCornerstoneViewport
// since it will trigger a rerender of each viewport and each resizing
// the offscreen canvas which would result in a performance hit, this should
// done only once per grid resize here. Doing it once here, allows us to reduce
// the refreshRage(in ms) to 10 from 50. I tried with even 1 or 5 ms it worked fine
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_SIZE_CHANGED, () => {
cornerstoneViewportService.resize(true);
});
initContextMenu({
cornerstoneViewportService,
customizationService,
@@ -211,88 +241,39 @@ export default async function init({
});
/**
* When a viewport gets a new display set, this call will go through all the
* active tools in the toolbar, and call any commands registered in the
* toolbar service with a callback to re-enable on displaying the viewport.
* Runs error handler for failed requests.
* @param event
*/
const toolbarEventListener = evt => {
const { element } = evt.detail;
const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
const toolData = toolbarService.getNestedButton(tool);
const commands = toolData?.listeners?.[evt.type];
commandsManager.run(commands, { element, evt });
});
};
/** Listens for active viewport events and fires the toolbar listeners */
const activeViewportEventListener = evt => {
const { viewportId } = evt;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const activeTools = toolbarService.getActiveTools();
activeTools.forEach(tool => {
if (!toolGroup?._toolInstances?.[tool]) {
return;
}
// check if tool is active on the new viewport
const toolEnabled = toolGroup._toolInstances[tool].mode === Enums.ToolModes.Enabled;
if (!toolEnabled) {
return;
}
const button = toolbarService.getNestedButton(tool);
const commands = button?.listeners?.[evt.type];
commandsManager.run(commands, { viewportId, evt });
});
};
const resetCrosshairs = evt => {
const { element } = evt.detail;
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(element);
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
if (!toolGroup || !toolGroup._toolInstances?.['Crosshairs']) {
return;
}
const mode = toolGroup._toolInstances['Crosshairs'].mode;
if (mode === Enums.ToolModes.Active) {
toolGroup.setToolActive('Crosshairs');
} else if (mode === Enums.ToolModes.Passive) {
toolGroup.setToolPassive('Crosshairs');
} else if (mode === Enums.ToolModes.Enabled) {
toolGroup.setToolEnabled('Crosshairs');
}
const imageLoadFailedHandler = ({ detail }) => {
const handler = errorHandler.getHTTPErrorHandler();
handler(detail.error);
};
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
});
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
function elementEnabledHandler(evt) {
const { element } = evt.detail;
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
element.addEventListener(EVENTS.CAMERA_RESET, evt => {
const { element } = evt.detail;
const { viewportId } = getEnabledElement(element);
commandsManager.runCommand('resetCrosshairs', { viewportId });
});
initViewTiming({ element, eventTarget });
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
initViewTiming({ element });
}
function elementDisabledHandler(evt) {
const { element } = evt.detail;
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// TODO - consider removing the callback when all elements are gone
// eventTarget.removeEventListener(
@@ -305,10 +286,7 @@ export default async function init({
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
activeViewportEventListener
);
colormaps.forEach(registerColormap);
}
function CPUModal() {
@@ -16,7 +16,6 @@ import {
ProbeTool,
AngleTool,
CobbAngleTool,
PlanarFreehandROITool,
MagnifyTool,
CrosshairsTool,
SegmentationDisplayTool,
@@ -28,6 +27,11 @@ import {
CircleScissorsTool,
RectangleScissorsTool,
SphereScissorsTool,
AdvancedMagnifyTool,
UltrasoundDirectionalTool,
PlanarFreehandROITool,
SplineROITool,
LivewireContourTool,
} from '@cornerstonejs/tools';
import CalibrationLineTool from './tools/CalibrationLineTool';
@@ -55,7 +59,6 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(DragProbeTool);
addTool(AngleTool);
addTool(CobbAngleTool);
addTool(PlanarFreehandROITool);
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
@@ -66,6 +69,11 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(RectangleScissorsTool);
addTool(SphereScissorsTool);
addTool(ImageOverlayViewerTool);
addTool(AdvancedMagnifyTool);
addTool(UltrasoundDirectionalTool);
addTool(PlanarFreehandROITool);
addTool(SplineROITool);
addTool(LivewireContourTool);
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
@@ -100,7 +108,6 @@ const toolNames = {
Bidirectional: BidirectionalTool.toolName,
Angle: AngleTool.toolName,
CobbAngle: CobbAngleTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
@@ -111,6 +118,11 @@ const toolNames = {
RectangleScissors: RectangleScissorsTool.toolName,
SphereScissors: SphereScissorsTool.toolName,
ImageOverlayViewer: ImageOverlayViewerTool.toolName,
AdvancedMagnify: AdvancedMagnifyTool.toolName,
UltrasoundDirectional: UltrasoundDirectionalTool.toolName,
SplineROI: SplineROITool.toolName,
LivewireContour: LivewireContourTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,
};
export { toolNames };
@@ -0,0 +1,267 @@
import { PubSubService } from '@ohif/core';
import { RENDERING_ENGINE_ID } from '../ViewportService/constants';
import { StackViewport, VolumeViewport, getRenderingEngine } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar';
const { ViewportColorbar } = utilities.voi.colorbar;
export default class ColorbarService extends PubSubService {
static EVENTS = {
STATE_CHANGED: 'event::ColorbarService:stateChanged',
};
static defaultStyles = {
position: 'absolute',
boxSizing: 'border-box',
border: 'solid 1px #555',
cursor: 'initial',
};
static positionStyles = {
left: { left: '5%' },
right: { right: '5%' },
top: { top: '5%' },
bottom: { bottom: '5%' },
};
static defaultTickStyles = {
position: 'left',
style: {
font: '12px Arial',
color: '#fff',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
public static REGISTRATION = {
name: 'colorbarService',
create: () => {
return new ColorbarService();
},
};
colorbars = {};
constructor() {
super(ColorbarService.EVENTS);
}
/**
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
*
* @param viewportId The identifier for the viewport where the colorbar will be added.
* @param displaySetInstanceUIDs An array of display set instance UIDs to associate with the colorbar.
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
*/
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
const { element } = viewport;
const actorEntries = viewport.getActors();
const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length;
const containers = this.createContainers(
numContainers,
element,
position,
thickness,
viewportId
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
if (!actorEntry) {
return;
}
const { uid: volumeId } = actorEntry;
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
// if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) {
this.setViewportColormap(
viewportId,
displaySetInstanceUID,
colormaps[activeColormapName],
true
);
}
const colorbarContainer = containers[index];
const colorbar = new ViewportColorbar({
id: `ctColorbar-${viewportId}-${index}`,
element,
colormaps: options.colormaps || {},
// if there's an existing colormap set, we use it, otherwise we use the activeColormapName, otherwise, grayscale
activeColormapName: colormap?.name || options?.activeColormapName || 'Grayscale',
container: colorbarContainer,
ticks: {
...ColorbarService.defaultTickStyles,
...options.ticks,
},
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
});
if (this.colorbars[viewportId]) {
this.colorbars[viewportId].push({ colorbar, container: colorbarContainer });
} else {
this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }];
}
});
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
viewportId,
changeType: ChangeTypes.Added,
});
}
/**
* Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references.
*
* @param viewportId The identifier for the viewport from which the colorbar will be removed.
*/
public removeColorbar(viewportId) {
const colorbarInfo = this.colorbars[viewportId];
if (!colorbarInfo) {
return;
}
colorbarInfo.forEach(({ colorbar, container }) => {
container.parentNode.removeChild(container);
});
delete this.colorbars[viewportId];
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
viewportId,
changeType: ChangeTypes.Removed,
});
}
/**
* Checks whether a colorbar is associated with a given viewport ID.
*
* @param viewportId The identifier for the viewport to check.
* @returns `true` if a colorbar exists for the specified viewport, otherwise `false`.
*/
public hasColorbar(viewportId) {
return this.colorbars[viewportId] ? true : false;
}
/**
* Retrieves the current state of colorbars, including all active colorbars and their configurations.
*
* @returns An object representing the current state of all colorbars managed by this service.
*/
public getState() {
return this.colorbars;
}
/**
* Retrieves colorbar information for a specific viewport ID.
*
* @param viewportId The identifier for the viewport to retrieve colorbar information for.
* @returns The colorbar information associated with the specified viewport, if available.
*/
public getViewportColorbar(viewportId) {
return this.colorbars[viewportId];
}
/**
* Handles the cleanup and removal of all colorbars from the viewports. This is typically called
* when exiting the mode or context in which the colorbars are used, ensuring that no DOM
* elements or references are left behind.
*/
public onModeExit() {
const viewportIds = Object.keys(this.colorbars);
viewportIds.forEach(viewportId => {
this.removeColorbar(viewportId);
});
}
/**
* Sets the colormap for a viewport. This function is used internally to update the colormap the viewport
*
* @param viewportId The identifier of the viewport to update.
* @param displaySetInstanceUID The display set instance UID associated with the viewport.
* @param colormap The colormap object to set on the viewport.
* @param immediate A boolean indicating whether the viewport should be re-rendered immediately after setting the colormap.
*/
private setViewportColormap(viewportId, displaySetInstanceUID, colormap, immediate = false) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
const actorEntries = viewport.getActors();
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);
}
if (viewport instanceof VolumeViewport) {
setViewportProperties(viewport, displaySetInstanceUID);
}
if (immediate) {
viewport.render();
}
}
/**
* Creates the container elements for colorbars based on the specified parameters. This function dynamically
* generates and styles DOM elements to host the colorbars, positioning them according to the specified options.
*
* @param numContainers The number of containers to create, typically corresponding to the number of colorbars.
* @param element The DOM element within which the colorbar containers will be placed.
* @param position The position of the colorbar containers (e.g., 'top', 'bottom', 'left', 'right').
* @param thickness The thickness of the colorbar containers, affecting their width or height depending on their position.
* @param viewportId The identifier of the viewport for which the containers are being created.
* @returns An array of the created container DOM elements.
*/
private createContainers(numContainers, element, position, thickness, viewportId) {
const containers = [];
const dimensions = {
1: 50,
2: 33,
};
const dimension = dimensions[numContainers] || 50 / numContainers;
Array.from({ length: numContainers }).forEach((_, i) => {
const colorbarContainer = document.createElement('div');
colorbarContainer.id = `ctColorbarContainer-${viewportId}-${i + 1}`;
Object.assign(colorbarContainer.style, ColorbarService.defaultStyles);
if (['top', 'bottom'].includes(position)) {
Object.assign(colorbarContainer.style, {
width: `${dimension}%`,
height: thickness || '2.5%',
left: `${(i + 1) * dimension}%`,
transform: 'translateX(-50%)',
...ColorbarService.positionStyles[position],
});
} else if (['left', 'right'].includes(position)) {
Object.assign(colorbarContainer.style, {
height: `${dimension}%`,
width: thickness || '2.5%',
top: `${(i + 1) * dimension}%`,
transform: 'translateY(-50%)',
...ColorbarService.positionStyles[position],
});
}
element.appendChild(colorbarContainer);
containers.push(colorbarContainer);
});
return containers;
}
}
@@ -0,0 +1,2 @@
import ColorbarService from './ColorbarService';
export default ColorbarService;
@@ -1,5 +1,3 @@
import cloneDeep from 'lodash.clonedeep';
import { Types as OhifTypes, ServicesManager, PubSubService } from '@ohif/core';
import {
cache,
@@ -11,7 +9,6 @@ import {
volumeLoader,
} from '@cornerstonejs/core';
import {
CONSTANTS as cstConstants,
Enums as csToolsEnums,
segmentation as cstSegmentation,
Types as cstTypes,
@@ -23,7 +20,6 @@ import { easeInOutBell, reverseEaseInOutBell } from '../../utils/transitions';
import { Segment, Segmentation, SegmentationConfig } from './SegmentationServiceTypes';
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
const { COLOR_LUT } = cstConstants;
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
@@ -465,15 +461,6 @@ class SegmentationService extends PubSubService {
},
]);
// if first segmentation, we can use the default colorLUT, otherwise
// we need to generate a new one and use a new colorLUT
const colorLUTIndex = 0;
if (Object.keys(this.segmentations).length !== 0) {
const newColorLUT = this.generateNewColorLUT();
const colorLUTIndex = this.getNextColorLUTIndex();
cstSegmentation.config.color.addColorLUT(newColorLUT, colorLUTIndex);
}
this.segmentations[segmentationId] = {
...segmentation,
label: segmentation.label || '',
@@ -482,7 +469,6 @@ class SegmentationService extends PubSubService {
segmentCount: segmentation.segmentCount ?? 0,
isActive: false,
isVisible: true,
colorLUTIndex,
};
cachedSegmentation = this.segmentations[segmentationId];
@@ -976,7 +962,7 @@ class SegmentationService extends PubSubService {
// Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
// it is easily compressible in worker thread.
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, {
volumeId: segmentationId,
targetBuffer: {
type: 'Uint8Array',
@@ -1002,6 +988,7 @@ class SegmentationService extends PubSubService {
referencedVolumeId: volumeId, // Todo: this is so ugly
},
},
description: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`,
};
this.addOrUpdateSegmentation(segmentation);
@@ -1037,8 +1024,6 @@ class SegmentationService extends PubSubService {
segmentation.hydrated = true;
}
const { colorLUTIndex } = segmentation;
// Based on the segmentationId, set the colorLUTIndex.
const segmentationRepresentationUIDs = await cstSegmentation.addSegmentationRepresentations(
toolGroupId,
@@ -1057,12 +1042,6 @@ class SegmentationService extends PubSubService {
segmentationRepresentationUIDs[0]
);
cstSegmentation.config.color.setColorLUT(
toolGroupId,
segmentationRepresentationUIDs[0],
colorLUTIndex
);
// add the segmentation segments properly
for (const segment of segmentation.segments) {
if (segment === null || segment === undefined) {
@@ -1390,8 +1369,6 @@ class SegmentationService extends PubSubService {
public setConfiguration = (configuration: SegmentationConfig): void => {
const {
brushSize,
brushThresholdGate,
fillAlpha,
fillAlphaInactive,
outlineWidthActive,
@@ -1483,7 +1460,6 @@ class SegmentationService extends PubSubService {
segmentInfo.label = label;
if (suppressEvents === false) {
// this._setSegmentationModified(segmentationId);
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
segmentation,
});
@@ -1530,7 +1506,6 @@ class SegmentationService extends PubSubService {
segments: [],
isVisible: true,
isActive: false,
colorLUTIndex: 0,
};
}
@@ -2124,23 +2099,6 @@ class SegmentationService extends PubSubService {
return viewportInfo.getToolGroupId();
};
private getNextColorLUTIndex = (): number => {
let i = 0;
while (true) {
if (cstSegmentation.state.getColorLUT(i) === undefined) {
return i;
}
i++;
}
};
private generateNewColorLUT() {
const newColorLUT = cloneDeep(COLOR_LUT);
return newColorLUT;
}
/**
* Converts object of objects to array.
*
@@ -20,6 +20,8 @@ type Segment = {
isVisible: boolean;
// whether the segment is locked
isLocked: boolean;
// display texts
displayText?: string[];
};
type Segmentation = {
@@ -67,4 +69,4 @@ type SegmentationRepresentationData = {
LABELMAP?: LabelmapSegmentationData;
};
export { SegmentationConfig, Segment, Segmentation };
export type { SegmentationConfig, Segment, Segmentation };
@@ -1,4 +1,5 @@
import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
import { getRenderingEngines } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
@@ -25,6 +26,7 @@ const POSITION = 'cameraposition';
const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -45,9 +47,14 @@ export default class SyncGroupService {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
// todo: remove stack image since it is legacy now and the image_slice
// handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
};
synchronizersByType: { [key: string]: Synchronizer[] } = {};
constructor(serviceManager: ServicesManager) {
this.servicesManager = serviceManager;
this.listeners = {};
@@ -57,14 +64,27 @@ export default class SyncGroupService {
}
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
return syncCreator(id, options);
const synchronizer = syncCreator(id, options);
if (synchronizer) {
this.synchronizersByType[type].push(synchronizer);
return synchronizer;
}
} else {
console.warn('Unknown synchronizer type', type, id);
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
}
}
public getSyncCreatorForType(type: string): SyncCreator {
return this.synchronizerCreators[type.toLowerCase()];
}
/**
* Creates a synchronizer type.
* @param type is the type of the synchronizer to create
@@ -74,6 +94,19 @@ export default class SyncGroupService {
this.synchronizerCreators[type.toLowerCase()] = creator;
}
public getSynchronizer(id: string): Synchronizer | void {
return SynchronizerManager.getSynchronizer(id);
}
/**
* Retrieves an array of synchronizers of a specific type.
* @param type - The type of synchronizers to retrieve.
* @returns An array of synchronizers of the specified type.
*/
public getSynchronizersOfType(type: string): Synchronizer[] {
return this.synchronizersByType[type];
}
protected _getOrCreateSynchronizer(
type: string,
id: string,
@@ -121,6 +154,20 @@ export default class SyncGroupService {
SynchronizerManager.destroy();
}
public getSynchronizersForViewport(viewportId: string): Synchronizer[] {
const renderingEngine =
getRenderingEngines().find(re => {
return re.getViewports().find(vp => vp.id === viewportId);
}) || getRenderingEngines()[0];
const synchronizers = SynchronizerManager.getAllSynchronizers();
return synchronizers.filter(
s =>
s.hasSourceViewport(renderingEngine.id, viewportId) ||
s.hasTargetViewport(renderingEngine.id, viewportId)
);
}
public removeViewportFromSyncGroup(
viewportId: string,
renderingEngineId: string,
@@ -189,19 +189,6 @@ export default class ToolGroupService {
this.addToolsToToolGroup(toolGroupId, tools);
return toolGroup;
}
/**
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
// handle specific assignment for volumeUID (e.g., fusion)
const toolInstances = toolGroup._toolInstances;
// Object.values(toolInstances).forEach(toolInstance => {
// if (toolInstance.configuration) {
// toolInstance.configuration.volumeUID = volumeUID;
// }
// });
}
*/
/**
* Get the tool's configuration based on the tool name and tool group id
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
@@ -0,0 +1,71 @@
import { PubSubService } from '@ohif/core';
import { ViewportActionCornersLocations } from '@ohif/ui';
import { ReactNode } from 'react';
export type ActionComponentInfo = {
viewportId: string;
id: string;
component: ReactNode;
location: ViewportActionCornersLocations;
indexPriority: number;
};
class ViewportActionCornersService extends PubSubService {
public static readonly EVENTS = {};
public static readonly LOCATIONS = ViewportActionCornersLocations;
public static REGISTRATION = {
name: 'viewportActionCornersService',
altName: 'ViewportActionCornersService',
create: ({ configuration = {} }) => {
return new ViewportActionCornersService();
},
};
serviceImplementation = {};
public LOCATIONS = ViewportActionCornersService.LOCATIONS;
constructor() {
super(ViewportActionCornersService.EVENTS);
this.serviceImplementation = {};
}
public setServiceImplementation({
getState: getStateImplementation,
setComponent: setComponentImplementation,
setComponents: setComponentsImplementation,
clear: clearImplementation,
}): void {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setComponentImplementation) {
this.serviceImplementation._setComponent = setComponentImplementation;
}
if (setComponentsImplementation) {
this.serviceImplementation._setComponents = setComponentsImplementation;
}
if (clearImplementation) {
this.serviceImplementation._clear = clearImplementation;
}
}
public getState() {
return this.serviceImplementation._getState();
}
public setComponent(component: ActionComponentInfo) {
this.serviceImplementation._setComponent(component);
}
public setComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components);
}
public clear(viewportId: string) {
this.serviceImplementation._clear(viewportId);
}
}
export default ViewportActionCornersService;
@@ -10,6 +10,7 @@ import {
VolumeViewport3D,
cache,
Enums as csEnums,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
@@ -17,7 +18,7 @@ import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { Presentation, Presentations } from '../../types/Presentation';
import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';
import JumpPresets from '../../utils/JumpPresets';
@@ -44,6 +45,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportsById: Map<string, ViewportInfo> = new Map();
viewportGridResizeObserver: ResizeObserver | null;
viewportsDisplaySets: Map<string, string[]> = new Map();
beforeResizePositionPresentations: Map<string, PositionPresentation> = new Map();
// Some configs
enableResizeDetector: true;
@@ -51,6 +53,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
resizeRefreshMode: 'debounce';
servicesManager = null;
resizeQueue = [];
viewportResizeTimer = null;
gridResizeDelay = 50;
gridResizeTimeOut = null;
constructor(servicesManager: ServicesManager) {
super(EVENTS);
this.renderingEngine = null;
@@ -94,14 +101,28 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
/**
* It triggers the resize on the rendering engine.
* It triggers the resize on the rendering engine, and renders the viewports
*
* @param isGridResize - if the resize is triggered by a grid resize
* this is used to avoid double resize of the viewports since if the
* grid is resized, all viewports will be resized so there is no need
* to resize them individually which will get triggered by their
* individual resize observers
*/
public resize() {
const immediate = true;
const keepCamera = true;
this.renderingEngine.resize(immediate, keepCamera);
this.renderingEngine.render();
public resize(isGridResize = false) {
// if there is a grid resize happening, it means the viewport grid
// has been manipulated (e.g., panels closed, added, etc.) and we need
// to resize all viewports, so we will add a timeout here to make sure
// we don't double resize the viewports when viewports in the grid are
// resized individually
if (isGridResize) {
this.performResize();
this.resetGridResizeTimeout();
this.resizeQueue = [];
clearTimeout(this.viewportResizeTimer);
} else {
this.enqueueViewportResizeRequest();
}
}
/**
@@ -138,74 +159,237 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.delete(viewportId);
}
public setPresentations(viewport, presentations?: Presentations): void {
const properties = presentations?.lutPresentation?.properties;
if (properties) {
viewport.setProperties(properties);
/**
* Sets the presentations for a given viewport. Presentations is an object
* that can define the lut or position for a viewport.
*
* @param viewportId - The ID of the viewport.
* @param presentations - The presentations to apply to the viewport.
*/
public setPresentations(viewportId: string, presentations?: Presentations): void {
const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
if (!viewport) {
return;
}
const camera = presentations?.positionPresentation?.camera;
if (camera) {
viewport.setCamera(camera);
if (!presentations) {
return;
}
const { lutPresentation, positionPresentation } = presentations;
if (lutPresentation) {
const { presentation } = lutPresentation;
if (viewport instanceof VolumeViewport) {
if (presentation instanceof Map) {
presentation.forEach((properties, volumeId) => {
viewport.setProperties(properties, volumeId);
});
} else {
viewport.setProperties(presentation);
}
} else {
viewport.setProperties(presentation);
}
}
if (positionPresentation) {
const { viewPlaneNormal, viewUp, zoom, pan } = positionPresentation.presentation;
viewport.setCamera({ viewPlaneNormal, viewUp });
if (zoom !== undefined) {
viewport.setZoom(zoom);
}
if (pan !== undefined) {
viewport.setPan(pan);
}
}
}
public getPresentation(viewportId: string): Presentation {
/**
* Retrieves the position presentation information for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The position presentation object containing various properties
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
*/
public getPositionPresentation(viewportId: string): PositionPresentation {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const { viewportType, presentationIds } = viewportInfo.getViewportOptions();
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { positionPresentationId } = presentationIds;
const csViewport = this.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const properties = csViewport.getProperties();
if (properties.isComputedVOI) {
delete properties.voiRange;
delete properties.VOILUTFunction;
}
const initialImageIndex = csViewport.getCurrentImageIdIndex();
const camera = csViewport.getCamera();
const { viewPlaneNormal, viewUp } = csViewport.getCamera();
const initialImageIndex = csViewport.getCurrentImageIdIndex() || 0;
const zoom = csViewport.getZoom();
const pan = csViewport.getPan();
return {
presentationIds,
viewportType: !viewportType || viewportType === 'stack' ? 'stack' : 'volume',
properties,
initialImageIndex,
camera,
id: positionPresentationId,
viewportType: viewportInfo.getViewportType(),
presentation: {
initialImageIndex,
viewUp,
viewPlaneNormal,
zoom,
pan,
},
};
}
public storePresentation({ viewportId }) {
const stateSyncService = this.servicesManager.services.stateSyncService;
let presentation;
try {
presentation = this.getPresentation(viewportId);
} catch (error) {
console.warn(error);
}
if (!presentation || !presentation.presentationIds) {
/**
* Retrieves the LUT (Lookup Table) presentation for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The LUT presentation object, or undefined if the viewport does not exist.
*/
public getLutPresentation(viewportId: string): LutPresentation {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = presentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { lutPresentationId } = presentationIds;
const csViewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
if (!csViewport) {
return;
}
const cleanProperties = properties => {
if (properties.isComputedVOI) {
delete properties.voiRange;
delete properties.VOILUTFunction;
}
return properties;
};
const presentation =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
if (presentation instanceof Map) {
csViewport.getActors().forEach(({ uid: volumeId }) => {
const properties = cleanProperties(csViewport.getProperties(volumeId));
presentation.set(volumeId, properties);
});
}
return {
id: lutPresentationId,
viewportType: viewportInfo.getViewportType(),
presentation,
};
}
/**
* Retrieves the presentations for a given viewport.
* @param viewportId - The ID of the viewport.
* @returns The presentations for the viewport.
*/
public getPresentations(viewportId: string): Presentations {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const positionPresentation = this.getPositionPresentation(viewportId);
const lutPresentation = this.getLutPresentation(viewportId);
return {
positionPresentation,
lutPresentation,
};
}
/**
* Stores the presentation state for a given viewport inside the
* stateSyncService. This is used to persist the presentation state
* across different scenarios e.g., when the viewport is changing the
* display set, or when the viewport is moving to a different layout.
*
* @param viewportId The ID of the viewport.
*/
public storePresentation({ viewportId }) {
let presentations = null as Presentations;
try {
presentations = this.getPresentations(viewportId);
if (!presentations?.positionPresentation && !presentations?.lutPresentation) {
return;
}
} catch (error) {
console.warn(error);
return;
}
const { stateSyncService, syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
stateSyncService.getState();
const { lutPresentation, positionPresentation } = presentations;
const { id: positionPresentationId } = positionPresentation;
const { id: lutPresentationId } = lutPresentation;
const updateStore = (store, id, value) => ({ ...store, [id]: value });
const newState = {} as { [key: string]: any };
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: presentation,
};
newState.lutPresentationStore = updateStore(
lutPresentationStore,
lutPresentationId,
lutPresentation
);
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: presentation,
};
newState.positionPresentationStore = updateStore(
positionPresentationStore,
positionPresentationId,
positionPresentation
);
}
stateSyncService.store(storeState);
if (synchronizers?.length) {
newState.synchronizersStore = updateStore(
synchronizersStore,
viewportId,
synchronizers.map(synchronizer => ({
id: synchronizer.id,
sourceViewports: [...synchronizer.getSourceViewports()],
targetViewports: [...synchronizer.getTargetViewports()],
}))
);
}
stateSyncService.store(newState);
}
/**
@@ -301,9 +485,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
public getCornerstoneViewport(
viewportId: string
): Types.IStackViewport | Types.IVolumeViewport | null {
/**
* Retrieves the Cornerstone viewport with the specified ID.
*
* @param viewportId - The ID of the viewport.
* @returns The Cornerstone viewport object if found, otherwise null.
*/
public getCornerstoneViewport(viewportId: string): Types.IViewport | null {
const viewportInfo = this.getViewportInfo(viewportId);
if (!viewportInfo || !this.renderingEngine || this.renderingEngine.hasBeenDestroyed) {
@@ -315,15 +503,51 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return viewport;
}
/**
* Retrieves the viewport information for a given viewport ID. The viewport information
* is the OHIF construct that holds different options and data for a given viewport and
* is different from the cornerstone viewport.
*
* @param viewportId The ID of the viewport.
* @returns The viewport information.
*/
public getViewportInfo(viewportId: string): ViewportInfo {
return this.viewportsById.get(viewportId);
}
/**
* Looks through the viewports to see if the specified measurement can be
* displayed in one of the viewports.
*
* @param measurement
* The measurement that is desired to view.
* @param activeViewportId - the index that was active at the time the jump
* was initiated.
* @return the viewportId that the measurement should be displayed in.
*/
public getViewportIdToJump(
activeViewportId: string,
displaySetInstanceUID: string,
cameraProps: unknown
): string {
const viewportInfo = this.getViewportInfo(activeViewportId);
const { referencedImageId } = cameraProps;
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
return activeViewportId;
}
return (
[...this.viewportsById.values()].find(viewportInfo =>
viewportInfo.contains(displaySetInstanceUID, referencedImageId)
)?.viewportId ?? null
);
}
private async _setStackViewport(
viewport: Types.IStackViewport,
viewportData: StackViewportData,
viewportInfo: ViewportInfo,
presentations: Presentations
presentations: Presentations = {}
): Promise<void> {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
@@ -340,7 +564,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const properties = { ...presentations.lutPresentation?.properties };
if (!presentations.lutPresentation?.properties) {
const { voi, voiInverted } = displaySetOptions[0];
const { voi, voiInverted, colormap } = displaySetOptions[0];
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
voi.windowWidth,
@@ -352,14 +576,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
if (voiInverted !== undefined) {
properties.invert = voiInverted;
}
if (colormap !== undefined) {
properties.colormap = colormap;
}
}
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
const camera = presentations.positionPresentation?.camera;
if (camera) {
viewport.setCamera(camera);
}
this.setPresentations(viewport.id, presentations);
});
}
@@ -427,7 +652,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewport: Types.IVolumeViewport,
viewportData: VolumeViewportData,
viewportInfo: ViewportInfo,
presentations: Presentations
presentations: Presentations = {}
): Promise<void> {
// TODO: We need to overhaul the way data sources work so requests can be made
// async. I think we should follow the image loader pattern which is async and
@@ -472,20 +697,24 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return hangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded);
if (volumesNotLoaded.length) {
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return hangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
});
}
volumesNotLoaded.forEach(volume => {
if (!volume.loadStatus.loading) {
volume.load();
}
});
}
volumeToLoad.forEach(volume => {
if (!volume.loadStatus.loaded && !volume.loadStatus.loading) {
volume.load();
}
});
// This returns the async continuation only
return this.setVolumesForViewport(viewport, volumeInputArray, presentations);
}
@@ -531,7 +760,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewport.setProperties(properties, volumeId);
});
this.setPresentations(viewport, presentations);
this.setPresentations(viewport.id, presentations);
// load any secondary displaySets
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
@@ -685,7 +914,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
_setDisplaySets(
viewport: StackViewport | VolumeViewport,
viewport: Types.IViewport,
viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo,
presentations: Presentations = {}
@@ -697,16 +926,18 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportInfo,
presentations
);
} else if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
}
if ([VolumeViewport, VolumeViewport3D].some(type => viewport instanceof type)) {
return this._setVolumeViewport(
viewport,
viewport as Types.IVolumeViewport,
viewportData as VolumeViewportData,
viewportInfo,
presentations
);
} else {
throw new Error('Unknown viewport type');
}
throw new Error('Unknown viewport type');
}
/**
@@ -772,32 +1003,57 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
}
/**
* Looks through the viewports to see if the specified measurement can be
* displayed in one of the viewports.
*
* @param measurement
* The measurement that is desired to view.
* @param activeViewportId - the index that was active at the time the jump
* was initiated.
* @return the viewportId that the measurement should be displayed in.
*/
public getViewportIdToJump(
activeViewportId: string,
displaySetInstanceUID: string,
cameraProps: unknown
): string {
const viewportInfo = this.getViewportInfo(activeViewportId);
const { referencedImageId } = cameraProps;
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
return activeViewportId;
private enqueueViewportResizeRequest() {
this.resizeQueue.push(false); // false indicates viewport resize
clearTimeout(this.viewportResizeTimer);
this.viewportResizeTimer = setTimeout(() => {
this.processViewportResizeQueue();
}, this.gridResizeDelay);
}
private processViewportResizeQueue() {
const isGridResizeInQueue = this.resizeQueue.some(isGridResize => isGridResize);
if (this.resizeQueue.length > 0 && !isGridResizeInQueue && !this.gridResizeTimeOut) {
this.performResize();
}
return (
[...this.viewportsById.values()].find(viewportInfo =>
viewportInfo.contains(displaySetInstanceUID, referencedImageId)
)?.viewportId ?? null
);
// Clear the queue after processing viewport resizes
this.resizeQueue = [];
}
private performResize() {
const isImmediate = false;
const viewports = this.getRenderingEngine().getViewports();
// Store the current position presentations for each viewport.
viewports.forEach(({ id }) => {
const presentation = this.getPositionPresentation(id);
this.beforeResizePositionPresentations.set(id, presentation);
});
// Resize the rendering engine and render.
const renderingEngine = this.renderingEngine;
renderingEngine.resize(isImmediate);
renderingEngine.render();
// Reset the camera for viewports that should reset their camera on resize,
// which means only those viewports that have a zoom level of 1.
this.beforeResizePositionPresentations.forEach((positionPresentation, viewportId) => {
this.setPresentations(viewportId, { positionPresentation });
});
// Resize and render the rendering engine again.
renderingEngine.resize(isImmediate);
renderingEngine.render();
}
private resetGridResizeTimeout() {
clearTimeout(this.gridResizeTimeOut);
this.gridResizeTimeOut = setTimeout(() => {
this.gridResizeTimeOut = null;
}, this.gridResizeDelay);
}
}
@@ -1,6 +1,7 @@
import { Types } from '@cornerstonejs/core';
import { StackData, VolumeData } from '../../types/CornerstoneCacheService';
import { DisplaySetOptions, PublicViewportOptions, ViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { Presentations } from '../../types/Presentation';
/**
* Handles cornerstone viewport logic including enabling, disabling, and
@@ -36,7 +37,7 @@ export interface IViewportService {
* the element for resizing events
* @param {*} elementRef
*/
resize(element: HTMLDivElement): void;
resize(isGridResize: boolean): void;
/**
* Removes the viewport from cornerstone, and destroys the rendering engine
*/
@@ -55,8 +56,10 @@ export interface IViewportService {
* @returns
*/
setViewportData(
viewportData: StackData | VolumeData,
viewportId: string,
viewportData: StackViewportData | VolumeViewportData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
publicDisplaySetOptions: DisplaySetOptions[],
presentations?: Presentations
): void;
}
@@ -1,5 +1,5 @@
import { Types, Enums } from '@cornerstonejs/core';
import { Types as UITypes } from '@ohif/ui';
import { Types as CoreTypes } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
@@ -18,7 +18,7 @@ export type ViewportOptions = {
toolGroupId: string;
viewportId: string;
// Presentation ID to store/load presentation state from
presentationIds?: UITypes.PresentationIds;
presentationIds?: CoreTypes.PresentationIds;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
displayArea?: Types.DisplayArea;
@@ -36,7 +36,7 @@ export type PublicViewportOptions = {
id?: string;
viewportType?: string;
toolGroupId?: string;
presentationIds?: UITypes.PresentationIds;
presentationIds?: CoreTypes.PresentationIds;
viewportId?: string;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
@@ -71,7 +71,7 @@ export type DisplaySetOptions = {
voiInverted: boolean;
blendMode?: Enums.BlendModes;
slabThickness?: number;
colormap?: string;
colormap?: { name: string; opacity?: number };
displayPreset?: string;
};
@@ -238,6 +238,11 @@ class ViewportInfo {
return this.viewportOptions;
}
public getPresentationIds(): CoreTypes.PresentationIds {
const { presentationIds } = this.viewportOptions;
return presentationIds;
}
public setDisplaySetOptions(displaySetOptions: Array<DisplaySetOptions>): void {
this.displaySetOptions = displaySetOptions;
}
@@ -1,8 +1,8 @@
import { VolumeViewport, metaData } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types';
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid } from '@ohif/core/src/utils';
import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
interface CachedStat {
color: number[]; // [r, g, b, a]
@@ -27,8 +27,12 @@ interface CachedStat {
*/
class ImageOverlayViewerTool extends AnnotationDisplayTool {
static toolName = 'ImageOverlayViewer';
private _cachedOverlayMetadata: Map<string, any[]> = new Map();
private _cachedStats: { [key: string]: CachedStat } = {};
/**
* The overlay plane module provider add method is exposed here to be used
* when updating the overlay for this tool to use for displaying data.
*/
public static addOverlayPlaneModule = OverlayPlaneModuleProvider.add;
constructor(
toolProps = {},
@@ -42,10 +46,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
super(toolProps, defaultToolProps);
}
onSetToolDisabled = (): void => {
this._cachedStats = {};
this._cachedOverlayMetadata = new Map();
};
onSetToolDisabled = (): void => {};
protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string {
if (viewport instanceof VolumeViewport) {
@@ -64,9 +65,8 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
return;
}
const overlays =
this._cachedOverlayMetadata.get(imageId) ??
metaData.get('overlayPlaneModule', imageId)?.overlays;
const overlayMetadata = metaData.get('overlayPlaneModule', imageId);
const overlays = overlayMetadata?.overlays;
// no overlays
if (!overlays?.length) {
@@ -79,9 +79,10 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
overlay.y ||= 0;
});
this._cachedOverlayMetadata.set(imageId, overlays);
// Will clear cached stat data when the overlay data changes
ImageOverlayViewerTool.addOverlayPlaneModule(imageId, overlayMetadata);
this._getCachedStat(imageId, overlays, this.configuration.fillColor).then(cachedStat => {
this._getCachedStat(imageId, overlayMetadata, this.configuration.fillColor).then(cachedStat => {
cachedStat.overlays.forEach(overlay => {
this._renderOverlay(enabledElement, svgDrawingHelper, overlay);
});
@@ -152,15 +153,18 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
private async _getCachedStat(
imageId: string,
overlayMetadata: any[],
overlayMetadata,
color: number[]
): Promise<CachedStat> {
if (this._cachedStats[imageId] && this._isSameColor(this._cachedStats[imageId].color, color)) {
return this._cachedStats[imageId];
const missingOverlay = overlayMetadata.overlays.filter(
overlay => overlay.pixelData && !overlay.dataUrl
);
if (missingOverlay.length === 0) {
return overlayMetadata;
}
const overlays = await Promise.all(
overlayMetadata
overlayMetadata.overlays
.filter(overlay => overlay.pixelData)
.map(async (overlay, idx) => {
let pixelData = null;
@@ -170,6 +174,10 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
pixelData = overlay.pixelData[0];
} else if (overlay.pixelData.retrieveBulkData) {
pixelData = await overlay.pixelData.retrieveBulkData();
} else if (overlay.pixelData.InlineBinary) {
const blob = b64toBlob(overlay.pixelData.InlineBinary);
const arrayBuffer = await blob.arrayBuffer();
pixelData = arrayBuffer;
}
if (!pixelData) {
@@ -178,7 +186,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
const dataUrl = this._renderOverlayToDataUrl(
{ width: overlay.columns, height: overlay.rows },
color,
overlay.color || color,
pixelData
);
@@ -190,13 +198,9 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool {
};
})
);
overlayMetadata.overlays = overlays;
this._cachedStats[imageId] = {
color: color,
overlays: overlays.filter(overlay => overlay),
};
return this._cachedStats[imageId];
return overlayMetadata;
}
/**
@@ -0,0 +1,40 @@
import { metaData } from '@cornerstonejs/core';
const _cachedOverlayMetadata: Map<string, any[]> = new Map();
/**
* Image Overlay Viewer tool is not a traditional tool that requires user interactin.
* But it is used to display Pixel Overlays. And it will provide toggling capability.
*
* The documentation for Overlay Plane Module of DICOM can be found in [C.9.2 of
* Part-3 of DICOM standard](https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.9.2.html)
*
* Image Overlay rendered by this tool can be toggled on and off using
* toolGroup.setToolEnabled() and toolGroup.setToolDisabled()
*/
const OverlayPlaneModuleProvider = {
/** Adds the metadata for overlayPlaneModule */
add: (imageId, metadata) => {
if (_cachedOverlayMetadata.get(imageId) === metadata) {
// This is a no-op here as the tool re-caches the data
return;
}
_cachedOverlayMetadata.set(imageId, metadata);
},
/** Standard getter for metadata */
get: (type: string, query: string | string[]) => {
if (Array.isArray(query)) {
return;
}
if (type !== 'overlayPlaneModule') {
return;
}
return _cachedOverlayMetadata.get(query);
},
};
// Needs to be higher priority than default provider
metaData.addProvider(OverlayPlaneModuleProvider.get, 10_000);
export default OverlayPlaneModuleProvider;
@@ -0,0 +1,32 @@
import { ColorMapPreset } from './Colormap';
import { CommandsManager, ServicesManager } from '@ohif/core';
export type ColorbarOptions = {
position: string;
colormaps: Array<ColorMapPreset>;
activeColormapName: string;
ticks: object;
width: string;
};
export type ColorbarProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
displaySets: Array<any>;
colorbarProperties: ColorbarProperties;
};
export type ColorbarProperties = {
width: string;
colorbarTickPosition: string;
colorbarContainerPosition: string;
colormaps: Array<ColorMapPreset>;
colorbarInitialColormap: string;
};
export enum ChangeTypes {
Removed = 'removed',
Added = 'added',
Modified = 'modified',
}
@@ -0,0 +1,16 @@
import { CommandsManager, ServicesManager } from '@ohif/core';
export type ColorMapPreset = {
ColorSpace;
description: string;
RGBPoints;
Name;
};
export type ColormapProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
colormaps: Array<ColorMapPreset>;
displaySets: Array<any>;
};
@@ -4,6 +4,7 @@ import SyncGroupService from '../services/SyncGroupService';
import SegmentationService from '../services/SegmentationService';
import CornerstoneCacheService from '../services/CornerstoneCacheService';
import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService';
import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService';
interface CornerstoneServices extends Types.Services {
cornerstoneViewportService: CornerstoneViewportService;
@@ -11,6 +12,7 @@ interface CornerstoneServices extends Types.Services {
syncGroupService: SyncGroupService;
segmentationService: SegmentationService;
cornerstoneCacheService: CornerstoneCacheService;
viewportActionCornersService: ViewportActionCornersService;
}
export default CornerstoneServices;
@@ -1,23 +1,45 @@
/** Store presentation data for either stack viewports or volume viewports */
import { Types } from '@cornerstonejs/core';
import { Types as UITypes } from '@ohif/ui';
import type { Types } from '@cornerstonejs/core';
/**
* Has information on the presentation of the viewport.
* Represents a position presentation in a viewport. This is basically
* viewport specific camera position and zoom, and not the display set
*/
export interface Presentation extends Types.StackViewportProperties {
presentationIds: UITypes.PresentationIds;
export type PositionPresentation = {
id: string;
viewportType: string;
initialImageIndex: number;
camera: Types.ICamera;
properties: Types.StackViewportProperties | Types.VolumeViewportProperties;
zoom?: number;
pan?: [number, number];
presentation: {
initialImageIndex: number;
viewUp: Types.Point3;
viewPlaneNormal: Types.Point3;
zoom?: number;
pan?: Types.Point2;
};
};
/**
* Represents a LUT presentation in a viewport, and is really related
* to displaySets and not the viewport itself. So that is why it can
* be an object with volumeId keys, or a single object with the properties
* itself
*/
export interface LutPresentation {
id: string;
viewportType: string;
presentation: Record<string, Types.ViewportProperties> | Types.ViewportProperties;
}
/**
* Presentation can be a PositionPresentation or a LutPresentation.
*/
type Presentation = PositionPresentation | LutPresentation;
/**
* Viewport presentations object that can contain a positionPresentation
* and or a lutPresentation.
*/
export type Presentations = {
positionPresentation?: Presentation;
lutPresentation?: Presentation;
positionPresentation?: PositionPresentation;
lutPresentation?: LutPresentation;
};
export default Presentation;
@@ -0,0 +1,66 @@
import { ServicesManager, CommandsManager } from '@ohif/core';
export type ViewportPreset = {
name: string;
gradientOpacity: string;
specularPower: string;
scalarOpacity: string;
specular: string;
shade: string;
ambient: string;
colorTransfer: string;
diffuse: string;
interpolation: string;
};
export type VolumeRenderingPresetsProps = {
viewportId: string;
serviceManager: ServicesManager;
commandsManager: CommandsManager;
volumeRenderingPresets: ViewportPreset[];
};
export type VolumeRenderingPresetsContentProps = {
presets: ViewportPreset[];
onClose: () => void;
viewportId: string;
commandsManager: CommandsManager;
};
export type VolumeRenderingOptionsProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export type VolumeRenderingQualityRange = {
min: number;
max: number;
step: number;
};
export type VolumeRenderingQualityProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
};
export type VolumeShiftProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
export type VolumeShadeProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
export type VolumeLightingProps = {
viewportId: string;
commandsManager: CommandsManager;
serviceManager: ServicesManager;
};
@@ -0,0 +1,5 @@
export type WindowLevelPreset = {
description: string;
window: string;
level: string;
};
@@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import html2canvas from 'html2canvas';
import {
Enums,
getEnabledElement,
getOrCreateCanvas,
StackViewport,
VolumeViewport,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
@@ -93,6 +93,7 @@ const CornerstoneViewportDownloadForm = ({
renderingEngine.resize();
// Trigger the render on the viewport to update the on screen
downloadViewport.resetCamera();
downloadViewport.render();
downloadViewportElement.addEventListener(
@@ -153,7 +154,7 @@ const CornerstoneViewportDownloadForm = ({
console.warn('Unable to set properties', e);
}
});
} else if (downloadViewport instanceof VolumeViewport) {
} else if (downloadViewport instanceof BaseVolumeViewport) {
const actors = viewport.getActors();
// downloadViewport.setActors(actors);
actors.forEach(actor => {
@@ -188,7 +189,7 @@ const CornerstoneViewportDownloadForm = ({
// add the viewport to the toolGroup
toolGroup.addViewport(downloadViewportId, renderingEngineId);
Object.keys(toolGroup._toolInstances).forEach(toolName => {
Object.keys(toolGroup.getToolInstances()).forEach(toolName => {
// make all tools Enabled so that they can not be interacted with
// in the download viewport
if (toggle && toolName !== 'Crosshairs') {
@@ -223,7 +224,6 @@ const CornerstoneViewportDownloadForm = ({
minimumSize={MINIMUM_SIZE}
maximumSize={MAX_TEXTURE_SIZE}
defaultSize={DEFAULT_SIZE}
canvasClass={'cornerstone-canvas'}
activeViewportElement={activeViewportElement}
enableViewport={enableViewport}
disableViewport={disableViewport}
@@ -1,5 +1,5 @@
import React from 'react';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
/**
*
@@ -13,6 +13,7 @@ import { Input, Dialog, ButtonEnums } from '@ohif/ui';
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
* @param {string?} dialogConfig.inputLabel - show label above the input
*/
function callInputDialog(
uiDialogService,
data,
@@ -88,4 +89,73 @@ function callInputDialog(
}
}
export function callLabelAutocompleteDialog(uiDialogService, callback, dialogConfig, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
const { validateFunc = value => true } = dialogConfig;
const labellingDoneCallback = value => {
if (typeof value === 'string') {
if (typeof validateFunc === 'function' && !validateFunc(value)) {
return;
}
callback(value, 'save');
} else {
callback('', 'cancel');
}
uiDialogService.dismiss({ id: 'select-annotation' });
};
uiDialogService.create({
id: 'select-annotation',
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
defaultPosition: {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
},
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: { label: '' },
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
}
export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
return new Promise<Map<any, any>>((resolve, reject) => {
const labellingDoneCallback = value => {
uiDialogService.dismiss({ id: 'select-annotation' });
if (typeof value === 'string') {
measurement.label = value;
}
resolve(measurement);
};
uiDialogService.create({
id: 'select-annotation',
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
defaultPosition: {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
},
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: measurement,
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
});
}
export default callInputDialog;
File diff suppressed because it is too large. Load diff
@@ -1,20 +1,35 @@
const STACK_SYNC_NAME = 'stackImageSync';
const IMAGE_SLICE_SYNC_NAME = 'IMAGE_SLICE_SYNC';
export default function toggleStackImageSync({
toggledState,
export default function toggleImageSliceSync({
servicesManager,
viewports: providedViewports,
syncId,
}) {
if (!toggledState) {
return disableSync(STACK_SYNC_NAME, servicesManager);
}
const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } =
servicesManager.services;
syncId ||= IMAGE_SLICE_SYNC_NAME;
const viewports =
providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService);
// Todo: right now we don't have a proper way to define specific
// viewports to add to synchronizers, and right now it is global or not
// after we do that, we should do fine grained control of the synchronizers
const someViewportHasSync = viewports.some(viewport => {
const syncStates = syncGroupService.getSynchronizersForViewport(
viewport.viewportOptions.viewportId
);
const imageSync = syncStates.find(syncState => syncState.id === syncId);
return !!imageSync;
});
if (someViewportHasSync) {
return disableSync(syncId, servicesManager);
}
// create synchronization group and add the viewports to it.
viewports.forEach(gridViewport => {
const { viewportId } = gridViewport.viewportOptions;
@@ -23,8 +38,8 @@ export default function toggleStackImageSync({
return;
}
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
type: 'stackimage',
id: STACK_SYNC_NAME,
type: 'imageSlice',
id: syncId,
source: true,
target: true,
});
@@ -1,13 +1,7 @@
import { log, Types } from '@ohif/core';
import { log, Enums } from '@ohif/core';
import { EVENTS } from '@cornerstonejs/core';
const { TimingEnum } = Types;
const IMAGE_TIMING_KEYS = [
TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES,
TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE,
TimingEnum.STUDY_TO_FIRST_IMAGE,
];
const IMAGE_TIMING_KEYS = [];
const imageTiming = {
viewportsWaiting: 0,
@@ -23,6 +17,18 @@ const imageTiming = {
*/
export default function initViewTiming({ element }) {
if (!IMAGE_TIMING_KEYS.length) {
// Work around a bug in WebPack that doesn't getting the enums initialized
// quite fast enough to be declared statically.
const { TimingEnum } = Enums;
IMAGE_TIMING_KEYS.push(
TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES,
TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE,
TimingEnum.STUDY_TO_FIRST_IMAGE,
);
}
if (!IMAGE_TIMING_KEYS.find(key => log.timingKeys[key])) {
return;
}
@@ -34,6 +40,7 @@ function imageRenderedListener(evt) {
if (evt.detail.viewportStatus === 'preRender') {
return;
}
const { TimingEnum } = Enums;
log.timeEnd(TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
log.timeEnd(TimingEnum.STUDY_TO_FIRST_IMAGE);
log.timeEnd(TimingEnum.SCRIPT_TO_VIEW);
@@ -50,7 +50,7 @@ const Angle = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -62,6 +62,7 @@ const Angle = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -48,7 +48,7 @@ const Length = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -59,6 +59,7 @@ const Length = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -45,7 +45,7 @@ const Bidirectional = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -57,6 +57,7 @@ const Bidirectional = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -43,7 +43,7 @@ const CircleROI = {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService);
@@ -55,6 +55,7 @@ const CircleROI = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -50,7 +50,7 @@ const CobbAngle = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -62,6 +62,7 @@ const CobbAngle = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -43,7 +43,7 @@ const EllipticalROI = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -55,6 +55,7 @@ const EllipticalROI = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -53,7 +53,7 @@ const Length = {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
@@ -65,6 +65,7 @@ const Length = {
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
Loaded 100 of 622 files, more files were not shown because too many files have changed in this diff. Show more