Compare commits

..
Author SHA1 Message Date
ohif-bot 6a54eee330 chore(version): Update package versions [skip ci] 2025-08-08 14:33:24 +00:00
ohif-bot 6b676750c0 chore(version): version.json [skip ci] 2025-08-08 14:33:18 +00:00
devin-ai-integration[bot]andAlireza f1d158bbe8 fix: Guard series.instances in DicomJSONDataSource to prevent TypeError (OHI-2114) (#5312)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2025-08-08 10:28:42 -04:00
ohif-bot 96862d64fc chore(version): Update package versions [skip ci] 2025-08-05 14:22:01 +00:00
ohif-bot 0f201ecf16 chore(version): version.json [skip ci] 2025-08-05 14:21:53 +00:00
Alireza 479d26f7c7 chore: remove manual approve (#5299) 2025-08-05 10:17:00 -04:00
ohif-bot 481c2ff781 chore(version): Update package versions [skip ci] 2025-08-05 14:11:38 +00:00
ohif-bot 0687120542 chore(version): version.json [skip ci] 2025-08-05 14:11:31 +00:00
Alireza 1bfb680b6b chore(ci): move to next version [BUMP BETA] (#5298) 2025-08-05 10:06:19 -04:00
ohif-bot 974ef29aa1 chore(version): Update package versions [skip ci] 2025-08-05 13:31:53 +00:00
ohif-bot a4d1f56c01 chore(version): version.json [skip ci] 2025-08-05 13:31:46 +00:00
Alireza 35bc074a53 fix(annotation): Add command to show pleura percentage in USAnnotationPanel and update condition for BLinePleuraPercentage (#5297) 2025-08-05 09:26:52 -04:00
ohif-bot 4d74611eb1 chore(version): Update package versions [skip ci] 2025-07-31 04:25:53 +00:00
ohif-bot 79cfc58533 chore(version): version.json [skip ci] 2025-07-31 04:25:47 +00:00
Pedro KöhlerandBill Wallace 735405a855 feat: add support to scoord3d (#5016)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-07-31 00:21:26 -04:00
ohif-bot af40f06524 chore(version): Update package versions [skip ci] 2025-07-30 14:12:33 +00:00
ohif-bot 568cd0b39d chore(version): version.json [skip ci] 2025-07-30 14:12:26 +00:00
rodrigobasilio2022 8643c868fd Adds containsAll validator (#5279) 2025-07-30 10:07:12 -04:00
ohif-bot eede2945ae chore(version): Update package versions [skip ci] 2025-07-30 02:35:07 +00:00
ohif-bot 1fc38c7379 chore(version): version.json [skip ci] 2025-07-30 02:35:00 +00:00
Pedro KöhlerandJoe Boccanfuso 271b84f78f feat: improve segment label (#5217)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2025-07-29 22:30:18 -04:00
ohif-bot dd896fea25 chore(version): Update package versions [skip ci] 2025-07-28 19:30:15 +00:00
ohif-bot 1e3f69d977 chore(version): version.json [skip ci] 2025-07-28 19:30:08 +00:00
Joe Boccanfuso 341f1c4ce7 fix(colorbar): Allow for AdvancedRenderingControls to be placed at the left or right of the viewport. (#5273) 2025-07-28 15:25:36 -04:00
ohif-bot b8ff0a80f0 chore(version): Update package versions [skip ci] 2025-07-28 16:25:07 +00:00
ohif-bot 760194ac80 chore(version): version.json [skip ci] 2025-07-28 16:24:59 +00:00
Joe Boccanfuso d24fe54d2b fix(colorbar): Allow for AdvancedRenderingControls to be placed at the top or bottom of the viewport. (#5270) 2025-07-28 12:19:58 -04:00
ohif-bot 1cc78112e5 chore(version): Update package versions [skip ci] 2025-07-28 13:42:43 +00:00
ohif-bot 0f816e4733 chore(version): version.json [skip ci] 2025-07-28 13:42:35 +00:00
Salim Kanoun 04dcacd3a5 fix: null save voi range properties (#5268) 2025-07-28 09:37:24 -04:00
Joe Boccanfuso 47e231f061 fix(multiframes): Corrected combining frame instances for multiframes. (#5264) 2025-07-28 09:35:29 -04:00
ohif-bot 4b7c26e273 chore(version): Update package versions [skip ci] 2025-07-28 05:41:04 +00:00
ohif-bot d45dc473ad chore(version): version.json [skip ci] 2025-07-28 05:40:57 +00:00
dxlin c766f3c054 test(SegmentationPanel): add test for jump to center of segments from PACS and basic add, rename, delete from panel (#5262) 2025-07-28 01:35:50 -04:00
ohif-bot 03941dca8e chore(version): Update package versions [skip ci] 2025-07-25 17:36:46 +00:00
ohif-bot be6e228c36 chore(version): version.json [skip ci] 2025-07-25 17:36:39 +00:00
Ibrahim 570e087007 fix(tooltips): tool tips should be portals (#5265) 2025-07-25 13:30:43 -04:00
ohif-bot afac23a79a chore(version): Update package versions [skip ci] 2025-07-25 13:47:18 +00:00
ohif-bot 3322f652fe chore(version): version.json [skip ci] 2025-07-25 13:47:10 +00:00
Emmanuel Ferdman d0cf9e02f4 fix: prevent failure in DICOM loader for wadouri type (#5260)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-07-25 09:41:53 -04:00
ohif-bot 84e3e43ae3 chore(version): Update package versions [skip ci] 2025-07-24 17:03:31 +00:00
ohif-bot 71b685364f chore(version): version.json [skip ci] 2025-07-24 17:03:24 +00:00
Joe Boccanfuso 3c8446a5e5 fix(ArrowAnnotate): Allow free-form text for arrow annotations (#5259) 2025-07-24 12:58:45 -04:00
ohif-bot 4e6530d1d8 chore(version): Update package versions [skip ci] 2025-07-23 22:13:34 +00:00
ohif-bot 0b87691d50 chore(version): version.json [skip ci] 2025-07-23 22:13:27 +00:00
Joe Boccanfuso 730046f83c Fix flaky SEGDataOverlayForUnreferencedDisplaySetNoHydration test. (#5254) 2025-07-23 18:08:42 -04:00
ohif-bot 763002f2fe chore(version): Update package versions [skip ci] 2025-07-23 19:10:24 +00:00
ohif-bot 76e630cf61 chore(version): version.json [skip ci] 2025-07-23 19:10:18 +00:00
arul-trenserandBill Wallace 428b6ffbe7 fix: issue introduced from PR 5022 (#5223)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-07-23 15:05:42 -04:00
ohif-bot 78597aacac chore(version): Update package versions [skip ci] 2025-07-23 18:20:06 +00:00
ohif-bot eacebad315 chore(version): version.json [skip ci] 2025-07-23 18:19:59 +00:00
MatheusRdkandCopilot c8261d9883 Handle 200 status response and add default error handling (#5227)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 14:14:08 -04:00
ohif-bot eedaa9a2e2 chore(version): Update package versions [skip ci] 2025-07-23 17:58:03 +00:00
ohif-bot fa37acc2fb chore(version): version.json [skip ci] 2025-07-23 17:57:56 +00:00
dependabot[bot]andJoe Boccanfuso 05526e5cdc chore(deps): bump axios from 1.10.0 to 1.11.0 (#5252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2025-07-23 13:53:02 -04:00
ohif-bot cf2e58bdb9 chore(version): Update package versions [skip ci] 2025-07-23 16:53:09 +00:00
ohif-bot 9b953f0942 chore(version): version.json [skip ci] 2025-07-23 16:53:02 +00:00
IbrahimandBill Wallace 75cc6eff32 fix(calibration): do not override user set calibration (#5231)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-07-23 12:48:29 -04:00
ohif-bot c271e869c2 chore(version): Update package versions [skip ci] 2025-07-23 16:27:02 +00:00
ohif-bot b71b14efdd chore(version): version.json [skip ci] 2025-07-23 16:26:55 +00:00
Teo Udovčić 7ffe17deea fix: disable seg download/export buttons until a user has drawn something (#4953) 2025-07-23 12:21:31 -04:00
ohif-bot dd2e89bfdb chore(version): Update package versions [skip ci] 2025-07-23 16:15:47 +00:00
ohif-bot 18f8c88a89 chore(version): version.json [skip ci] 2025-07-23 16:15:40 +00:00
Patrick WespiandJoe Boccanfuso bd3ff5f57c fix(ViewportOrientationMarkers): round rotation to fix floating point issue (#5135)
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2025-07-23 12:10:49 -04:00
ohif-bot 04a599962d chore(version): Update package versions [skip ci] 2025-07-23 14:55:46 +00:00
ohif-bot 3be5d84fc4 chore(version): version.json [skip ci] 2025-07-23 14:55:40 +00:00
Joe Boccanfuso 83d4bc4a5a fix(security): Removed stylus package due to potential security vulnerability and we don't use it. (#5250) 2025-07-23 10:50:53 -04:00
Joe Boccanfuso 83ea0ad93e fix(docs): Added docs for multimonitor and local linking (#5247) 2025-07-23 10:14:01 -04:00
ohif-bot b77b2f3fa0 chore(version): Update package versions [skip ci] 2025-07-22 17:35:21 +00:00
ohif-bot 111c3b3d0d chore(version): version.json [skip ci] 2025-07-22 17:35:15 +00:00
76a2b90c17 chore(deps): bump form-data from 4.0.3 to 4.0.4 (#5244)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2025-07-22 13:30:29 -04:00
ohif-bot aa83cda49c chore(version): Update package versions [skip ci] 2025-07-22 12:36:09 +00:00
ohif-bot 1ae4967fe5 chore(version): version.json [skip ci] 2025-07-22 12:36:02 +00:00
arul-trenser 12de7a85c9 feat(segmentation): Add customization for handling missing referencedDisplaySetInstanceUID for the SEG/RTSTRUCT. (#4983) 2025-07-22 08:30:54 -04:00
ohif-bot 91d0ae7da6 chore(version): Update package versions [skip ci] 2025-07-21 14:49:45 +00:00
ohif-bot f7fab0c461 chore(version): version.json [skip ci] 2025-07-21 14:49:38 +00:00
Joe Boccanfuso bde0eb1f07 fix(docs): Added entry for the showStudyList configuration setting. (#5235) 2025-07-21 10:44:28 -04:00
ohif-bot 73ee3140ea chore(version): Update package versions [skip ci] 2025-07-21 14:20:21 +00:00
ohif-bot 2a738dec09 chore(version): version.json [skip ci] 2025-07-21 14:20:15 +00:00
Joe Boccanfuso 3d5ee687cb fix(security): Pin eslint-config-prettier and eslint-plugin-prettier versions. (#5234) 2025-07-21 10:15:28 -04:00
ohif-bot 7060aeca4b chore(version): Update package versions [skip ci] 2025-07-21 13:11:39 +00:00
ohif-bot 5ecb8e9821 chore(version): version.json [skip ci] 2025-07-21 13:11:32 +00:00
Salim Kanoun ed3de96e4c fix: labelmap init (#5228) 2025-07-21 09:06:34 -04:00
ohif-bot db8a54d965 chore(version): Update package versions [skip ci] 2025-07-18 16:13:18 +00:00
ohif-bot 3980150aef chore(version): version.json [skip ci] 2025-07-18 16:13:10 +00:00
Joe Boccanfuso 3d4ec3181d fix(multimonitor): Study browser more menu fixes (#5208) 2025-07-18 12:08:18 -04:00
ohif-bot 779907e526 chore(version): Update package versions [skip ci] 2025-07-17 12:16:46 +00:00
ohif-bot 1841d6202f chore(version): version.json [skip ci] 2025-07-17 12:16:38 +00:00
arul-trenser 6a4ca70836 fix: image slices are displayed in reverse order, if we switch from common layout to 2D MPR (#5022) 2025-07-17 08:11:26 -04:00
ohif-bot 8dda7d5f81 chore(version): Update package versions [skip ci] 2025-07-16 18:45:48 +00:00
ohif-bot e207f58031 chore(version): version.json [skip ci] 2025-07-16 18:45:41 +00:00
Ibrahim faef74ab21 fix(viewportgrid): don't subscribe to duplicate event listeners (#5218) 2025-07-16 14:40:47 -04:00
Ibrahim 3ff9a65ef3 fix(black-viewports): MPR viewports were turning black if their parent stack had it's properties reset (#5219) 2025-07-16 14:40:20 -04:00
Alexey Klimovich 26d77fb149 fix(DicomUpload): Fix upload form and some scrollbars styles (#5125) 2025-07-16 14:38:11 -04:00
ohif-bot 5977326665 chore(version): Update package versions [skip ci] 2025-07-16 17:18:03 +00:00
ohif-bot 27e4f0440c chore(version): version.json [skip ci] 2025-07-16 17:17:56 +00:00
Vishnu Suresh Perumbavoor f38805e6f1 fix(segmentation): Fix segmentationid undefined 5106 (#5107) 2025-07-16 13:12:47 -04:00
ohif-bot a7892c0d59 chore(version): Update package versions [skip ci] 2025-07-16 00:15:39 +00:00
ohif-bot 991f17e0ca chore(version): version.json [skip ci] 2025-07-16 00:15:32 +00:00
Joe Boccanfuso 522d2aa764 fix(TMTV): The TMTV CSV file now has the correct name and contents (#5215) 2025-07-15 20:09:23 -04:00
ohif-bot fceb515a68 chore(version): Update package versions [skip ci] 2025-07-15 13:07:36 +00:00
ohif-bot e1fb97f862 chore(version): version.json [skip ci] 2025-07-15 13:07:29 +00:00
Joe Boccanfuso e0289dc49f fix(segmentation): Removed LOAD/hydrate button for segmentation overlays (#5214) 2025-07-15 09:02:36 -04:00
ohif-bot 302705ea4d chore(version): Update package versions [skip ci] 2025-07-14 15:55:44 +00:00
ohif-bot ff0568f2ab chore(version): version.json [skip ci] 2025-07-14 15:55:36 +00:00
Joe Boccanfuso 545411137c fix(panels): Allow for setting the initial and minimum expanded widths of each side panel. (#5207) 2025-07-14 11:50:27 -04:00
ohif-bot 1f33b291b9 chore(version): Update package versions [skip ci] 2025-07-14 13:15:07 +00:00
ohif-bot 768e5ba928 chore(version): version.json [skip ci] 2025-07-14 13:15:00 +00:00
c175837273 feat(viewport): Enhance Orientation Menu with Reformat Option and UI Improvements (#5184)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2025-07-14 09:10:09 -04:00
ohif-bot 529806c0d9 chore(version): Update package versions [skip ci] 2025-07-14 02:39:23 +00:00
ohif-bot 658d824505 chore(version): version.json [skip ci] 2025-07-14 02:39:16 +00:00
Joe Boccanfuso 94bdcf414d fix(UI): Restored the scroll bar OHIF L&F (#5202) 2025-07-13 22:34:18 -04:00
ohif-bot a24ec13239 chore(version): Update package versions [skip ci] 2025-07-11 21:20:32 +00:00
ohif-bot 2c7c62443c chore(version): version.json [skip ci] 2025-07-11 21:20:25 +00:00
rodrigobasilio2022 540923ad8b fix: enhance preview accept method (#5199) 2025-07-11 17:15:26 -04:00
ohif-bot df926e2513 chore(version): Update package versions [skip ci] 2025-07-11 20:43:30 +00:00
ohif-bot 86ad50d886 chore(version): version.json [skip ci] 2025-07-11 20:43:23 +00:00
Dan Rukas 0ed9ef06c1 ui(icons): Adds action and helper icon artwork for segmentation (#5200) 2025-07-11 16:38:11 -04:00
Vinícius Alves de Faria Resende 4a59126dd9 fix(UnitTests): Fixing "Cannot find module @cornerstonejs/core" error while running unit tests (#5148) 2025-07-11 16:36:49 -04:00
ohif-bot 277cef2168 chore(version): Update package versions [skip ci] 2025-07-11 17:49:56 +00:00
ohif-bot 168d5b27f3 chore(version): version.json [skip ci] 2025-07-11 17:49:48 +00:00
rodrigobasilio2022 fd5bcee909 fix: exception in newly added segmentations (#5198) 2025-07-11 13:44:22 -04:00
ohif-bot 8bbf83fb84 chore(version): Update package versions [skip ci] 2025-07-11 16:38:14 +00:00
ohif-bot 4253982050 chore(version): version.json [skip ci] 2025-07-11 16:38:07 +00:00
Ibrahim 10d93161b4 feat(rendering): Introduce WebGLContextPool for parallel rendering in ContextPoolRenderingEngine (#5196) 2025-07-11 12:33:24 -04:00
ohif-bot 7d3e222182 chore(version): Update package versions [skip ci] 2025-07-11 15:30:57 +00:00
ohif-bot 46df245fb8 chore(version): version.json [skip ci] 2025-07-11 15:30:50 +00:00
Pedro Köhler 4d3ff5a6cc feat: group clear measurements history (#5194) 2025-07-11 11:26:10 -04:00
ohif-bot 4e99f1ed34 chore(version): Update package versions [skip ci] 2025-07-11 13:48:44 +00:00
ohif-bot 2ddeed981d chore(version): version.json [skip ci] 2025-07-11 13:48:38 +00:00
Joe Boccanfuso eeaf49c61a fix(security): Patch for Babel vulnerability (#5193) 2025-07-11 09:43:56 -04:00
ohif-bot 2c1e8cb3e1 chore(version): Update package versions [skip ci] 2025-07-10 14:28:05 +00:00
ohif-bot f772be8ae3 chore(version): version.json [skip ci] 2025-07-10 14:27:58 +00:00
Ibrahim 4c5e34d183 fix(rendering): Introduce SequentialRenderingEngine to resolve canvas size limitation, performance degradation on high res monitors and enhance multi-monitor support (#5195) 2025-07-10 10:23:09 -04:00
ohif-bot 0c62bd5e4e chore(version): Update package versions [skip ci] 2025-07-10 13:38:17 +00:00
ohif-bot 4424d2a2bc chore(version): version.json [skip ci] 2025-07-10 13:38:10 +00:00
Joe Boccanfuso 48473625f6 fix(segmentation overlays): Allow for the addition of multiple segmentation overlays. (#5189) 2025-07-10 09:33:39 -04:00
ohif-bot 32ab291305 chore(version): Update package versions [skip ci] 2025-07-09 18:16:44 +00:00
ohif-bot 93eb0dee19 chore(version): version.json [skip ci] 2025-07-09 18:16:38 +00:00
Pedro Köhler ac852485cd feat: add segment label tool (#5164) 2025-07-09 14:11:54 -04:00
ohif-bot d19cd6ec7b chore(version): Update package versions [skip ci] 2025-07-08 14:25:44 +00:00
ohif-bot 4556dd9713 chore(version): version.json [skip ci] 2025-07-08 14:25:37 +00:00
Ibrahim 7b2e37a44f fix(measurement-group): use measurment study uid instead of active study uid (#5091) 2025-07-08 10:20:49 -04:00
Ibrahim 64a634cab9 fix(labeling): measurement labeling dialog was overwriting the entire measurement and breaking it (#5095) 2025-07-08 10:19:18 -04:00
ohif-bot ca7defc668 chore(version): Update package versions [skip ci] 2025-07-07 22:14:28 +00:00
ohif-bot e5b95de0b0 chore(version): version.json [skip ci] 2025-07-07 22:14:21 +00:00
Joe Boccanfuso f08cc303e6 fix(overlay-menu): Adding, changing and removing segmentation overlays should update the overlay menu instantly (#5181) 2025-07-07 18:09:32 -04:00
ohif-bot b8b14b8940 chore(version): Update package versions [skip ci] 2025-07-03 18:19:12 +00:00
ohif-bot 7f19db7f0e chore(version): version.json [skip ci] 2025-07-03 18:19:05 +00:00
Salim Kanoun e277303395 fix: add force to remove image from cache (#5176) 2025-07-03 14:14:19 -04:00
ohif-bot f7cf356d46 chore(version): Update package versions [skip ci] 2025-07-03 18:13:29 +00:00
ohif-bot baad251259 chore(version): version.json [skip ci] 2025-07-03 18:13:23 +00:00
24e0263fb9 chore: recovery 2 (#5014) merge to 3.11 (#5175)
Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Tang Cheng <45505657+tctco@users.noreply.github.com>
2025-07-03 14:08:46 -04:00
ohif-bot 17ed63eb1d chore(version): Update package versions [skip ci] 2025-07-03 02:57:54 +00:00
ohif-bot 6f8592a449 chore(version): version.json [skip ci] 2025-07-03 02:57:48 +00:00
Celian-abd c112db13a8 feat: Add possibility to add additional props for PanelSection (#5172) 2025-07-02 22:52:49 -04:00
ohif-bot b0704b88cd chore(version): Update package versions [skip ci] 2025-07-02 12:43:01 +00:00
ohif-bot 8491407319 chore(version): version.json [skip ci] 2025-07-02 12:42:54 +00:00
Joe Boccanfuso 4f6f5254fb fix(segmentation): Added support for unhydrated RTSTRUCT in MPR. Added support for overlaying segmentations over display sets the segmentations do not reference. (#5168) 2025-07-02 08:38:22 -04:00
ohif-bot 77758fde4f chore(version): Update package versions [skip ci] 2025-06-27 17:13:02 +00:00
ohif-bot 90943b4dc5 chore(version): version.json [skip ci] 2025-06-27 17:12:55 +00:00
Ibrahim 4bddd53fcd feat(hp): add onStageChange callback (#5165) 2025-06-27 13:08:21 -04:00
ohif-bot c2b75e1a50 chore(version): Update package versions [skip ci] 2025-06-27 11:16:22 +00:00
ohif-bot 51f17a92c1 chore(version): version.json [skip ci] 2025-06-27 11:16:15 +00:00
Joe Boccanfuso c2624f116d fix(segmentation): Changes to fix problems with non hydrated/loaded segmentations to be viewable when switching hanging protocols (e.g. MPR) (#5139) 2025-06-27 07:11:46 -04:00
ohif-bot 9a81f8cef5 chore(version): Update package versions [skip ci] 2025-06-27 03:02:26 +00:00
ohif-bot 8a35453100 chore(version): version.json [skip ci] 2025-06-27 03:02:20 +00:00
dependabot[bot] b660a5d523 chore(deps): bump webpack to 5.95.0 (#5163) 2025-06-26 22:57:43 -04:00
ohif-bot da8ecdd88c chore(version): Update package versions [skip ci] 2025-06-26 20:24:13 +00:00
ohif-bot ef5ce6c19b chore(version): version.json [skip ci] 2025-06-26 20:24:06 +00:00
dependabot[bot] c063fe8550 chore(deps-dev): bump webpack from 5.89.0 to 5.94.0 in /extensions/usAnnotation (#5162) 2025-06-26 16:18:57 -04:00
ohif-bot 5524d1e845 chore(version): Update package versions [skip ci] 2025-06-26 16:04:50 +00:00
ohif-bot 27eb442f62 chore(version): version.json [skip ci] 2025-06-26 16:04:44 +00:00
rodrigobasilio2022 9824f79177 feat(labelmap): Add labelmap segmentation in OHIF (#5158) 2025-06-26 11:59:44 -04:00
ohif-bot e07bb212bb chore(version): Update package versions [skip ci] 2025-06-26 12:48:28 +00:00
ohif-bot 2a06a3c17f chore(version): version.json [skip ci] 2025-06-26 12:48:21 +00:00
dependabot[bot] 4c1356cab7 chore(deps-dev): bump webpack from 5.89.0 to 5.94.0 in /modes/usAnnotation (#5156) 2025-06-26 08:43:27 -04:00
ohif-bot a17fb5aeba chore(version): Update package versions [skip ci] 2025-06-25 22:43:05 +00:00
ohif-bot b919ebbc41 chore(version): version.json [skip ci] 2025-06-25 22:42:59 +00:00
Joe Boccanfuso ec0c4f7451 fix(unit-tests): Update bun.lock to fix unit tests. (#5159) 2025-06-25 18:38:36 -04:00
ohif-bot 746781f6a4 chore(version): Update package versions [skip ci] 2025-06-25 22:13:21 +00:00
ohif-bot 332d498d2c chore(version): version.json [skip ci] 2025-06-25 22:13:14 +00:00
Alireza 6c9d99eef8 fix(build): BUILD_PACKAGES_QUICK PR check (#5160) 2025-06-25 18:07:52 -04:00
Ibrahim 57f07ad9f2 feat(toolbar/hokeys): add pub sub events to hotkeys manager class, subscribe toolbar to hotkeys pressed event (#5157) 2025-06-25 10:43:27 -04:00
dxlin b1ad3787e4 test(MeasurementPanel): Converted MeasurementPanel interaction tests from cypress to playwright (#5153) 2025-06-24 23:33:21 -04:00
rodrigobasilio2022 226f9b0773 feat: Ultrasound Pleura B-line Annotation Mode & Extension (#5061) 2025-06-24 12:50:48 -04:00
ohif-bot 9b8eef9474 chore(version): Update package versions [skip ci] 2025-06-24 15:50:18 +00:00
ohif-bot 1e605027ff chore(version): version.json [skip ci] 2025-06-24 15:50:11 +00:00
b48aab263f chore(deps-dev): bump webpack-dev-server from 4.7.3 to 5.2.1 (#5114)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2025-06-24 11:45:29 -04:00
dependabot[bot] 08d978bd75 chore(deps): bump pbkdf2 from 3.1.2 to 3.1.3 (#5152)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 11:43:19 -04:00
ohif-bot 4857e0002b chore(version): Update package versions [skip ci] 2025-06-20 15:31:20 +00:00
ohif-bot 9ed2d88cb1 chore(version): version.json [skip ci] 2025-06-20 15:31:13 +00:00
Ibrahim d6d3fa7ef4 fix(action-corners): check hideWhenDisabled at both the evaluator and prop levels (#5149) 2025-06-20 11:26:26 -04:00
ohif-bot 3f1ea26eba chore(version): Update package versions [skip ci] 2025-06-18 18:20:36 +00:00
ohif-bot 9f80732674 chore(version): version.json [skip ci] 2025-06-18 18:20:30 +00:00
Vinícius Alves de Faria Resende 268d62e991 fix(UnitTest): run was failing due to JS tests importing TS modules (#5145) 2025-06-18 14:15:50 -04:00
ohif-bot f6337e0970 chore(version): Update package versions [skip ci] 2025-06-18 03:08:58 +00:00
ohif-bot 2ed6172861 chore(version): version.json [skip ci] 2025-06-18 03:08:52 +00:00
Patrick Wespi dc829c8fca fix(getToolbarModule): fix enabled state of image slice sync (#5136) 2025-06-17 23:03:51 -04:00
ohif-bot afc2ca0dce chore(version): Update package versions [skip ci] 2025-06-13 14:48:30 +00:00
ohif-bot abb6da4458 chore(version): version.json [skip ci] 2025-06-13 14:48:22 +00:00
Joe BoccanfusoandBill Wallace 5311c941f7 fix(hp): When reapplying an hp the display set selectors should be considered. (#5074)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-06-13 10:43:31 -04:00
ohif-bot 4199d788a8 chore(version): Update package versions [skip ci] 2025-06-11 16:41:58 +00:00
ohif-bot 40eac04428 chore(version): version.json [skip ci] 2025-06-11 16:41:51 +00:00
dependabot[bot] abaf6582e0 chore(deps): bump tar-fs from 2.1.2 to 2.1.3 (#5096)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 12:37:24 -04:00
ohif-bot ce70c8401a chore(version): Update package versions [skip ci] 2025-06-11 13:05:36 +00:00
ohif-bot a906eb04ab chore(version): version.json [skip ci] 2025-06-11 13:05:29 +00:00
DanandDan Elkis d3c9ff50c7 fix(ToolbarService): Fixed removeButton bug for removing buttons inside sections (#5123)
Co-authored-by: Dan Elkis <dan.elkis@signalpet.com>
2025-06-11 09:00:54 -04:00
DanandDan Elkis 4da7fc307f feat(commandsModule): Enhanced flipViewport and rotateViewport commands (#5065)
Co-authored-by: Dan Elkis <dan.elkis@signalpet.com>
2025-06-11 08:59:37 -04:00
ohif-bot 68bbe0b1a5 chore(version): Update package versions [skip ci] 2025-06-11 11:41:43 +00:00
ohif-bot d15494f80c chore(version): version.json [skip ci] 2025-06-11 11:41:36 +00:00
Bill Wallace 75271ca416 fix: data-viewportId name is illegal, renamed to data-viewportid (#5121) 2025-06-11 07:36:26 -04:00
ohif-bot a83bd298f5 chore(version): Update package versions [skip ci] 2025-06-10 15:52:05 +00:00
ohif-bot 8ff59dacb5 chore(version): version.json [skip ci] 2025-06-10 15:51:59 +00:00
IbrahimandBill Wallace 26fd51f535 fix(markerlabelmap): accept preview/reject preview not working when the marker annotation tools are active (#5101)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-06-10 11:47:20 -04:00
ohif-bot 10ef509ff1 chore(version): Update package versions [skip ci] 2025-06-09 13:32:53 +00:00
ohif-bot 735a13a85b chore(version): version.json [skip ci] 2025-06-09 13:32:47 +00:00
Joe Boccanfuso a361e556ef fix(touch): Prevent touch drag to refresh the browser page. (#5108) 2025-06-09 09:28:00 -04:00
ohif-bot 9baf153d0d chore(version): Update package versions [skip ci] 2025-06-09 12:34:55 +00:00
ohif-bot b99f64da1b chore(version): version.json [skip ci] 2025-06-09 12:34:48 +00:00
Alireza b194f672c0 docs: improved migration guides for 3p10 (#5110) 2025-06-09 08:30:20 -04:00
ohif-bot 635877f184 chore(version): Update package versions [skip ci] 2025-06-07 17:59:59 +00:00
ohif-bot 2306ec2a6d chore(version): version.json [skip ci] 2025-06-07 17:59:53 +00:00
Joe Boccanfuso 0afb3fc753 fix(WindowLevelActionMenu): The window level menu must show different contents for 3D volume viewports (#5113) 2025-06-07 13:55:33 -04:00
ohif-bot 2234121a19 chore(version): Update package versions [skip ci] 2025-06-05 17:55:24 +00:00
ohif-bot a1ecc826aa chore(version): version.json [skip ci] 2025-06-05 17:55:17 +00:00
Joe Boccanfuso 1a6aa09a77 fix(touch): Allow for multi-touch in OHIF (#5099) 2025-06-05 13:50:27 -04:00
ohif-bot 2e7124bace chore(version): Update package versions [skip ci] 2025-06-02 17:59:54 +00:00
ohif-bot 2ddfc95bfa chore(version): version.json [skip ci] 2025-06-02 17:59:48 +00:00
Joe Boccanfuso 316d5a7644 fix(tmtv): Corrected the various section names being referenced. (#5093) 2025-06-02 13:55:32 -04:00
ohif-bot 01c7d45c15 chore(version): Update package versions [skip ci] 2025-06-02 16:17:00 +00:00
ohif-bot e804d7473e chore(version): version.json [skip ci] 2025-06-02 16:16:54 +00:00
Joe Boccanfuso 38ed7ba744 fix(segmentation): Allow for debounced pub/sub events to be reliably cancelled when unsubscribed to. (#5092) 2025-06-02 12:12:31 -04:00
ohif-bot a1fb7217bd chore(version): Update package versions [skip ci] 2025-05-30 14:16:22 +00:00
ohif-bot 8adbeb54f9 chore(version): version.json [skip ci] 2025-05-30 14:16:16 +00:00
DanandDan Elkis 8b2f80b82e docs: Fixed incorrect documentation and variables names (#5080)
Co-authored-by: Dan Elkis <dan.elkis@signalpet.com>
2025-05-30 10:11:16 -04:00
ohif-bot 31e19ffe8c chore(version): Update package versions [skip ci] 2025-05-29 12:30:56 +00:00
ohif-bot 4f6c8766b1 chore(version): version.json [skip ci] 2025-05-29 12:30:49 +00:00
Pedro Köhler 5891505c51 feat: create memo when manually adding and removing annotations (#5051) 2025-05-29 08:26:16 -04:00
ohif-bot 9afded1720 chore(version): Update package versions [skip ci] 2025-05-29 02:45:33 +00:00
ohif-bot 60e6e686ec chore(version): version.json [skip ci] 2025-05-29 02:45:26 +00:00
Alireza c6327acdc1 fix(ui): Prevent unintended text selection and improve component behavior (#5077) 2025-05-28 22:41:15 -04:00
ohif-bot 773d6edaf7 chore(version): Update package versions [skip ci] 2025-05-28 13:13:03 +00:00
ohif-bot 843d99d9ef chore(version): version.json [skip ci] 2025-05-28 13:12:56 +00:00
DanandDan Elkis c88bdac4b1 fix(Customization): Fixed incorrect events broadcast (#5075)
Co-authored-by: Dan Elkis <dan.elkis@signalpet.com>
2025-05-28 09:08:05 -04:00
ohif-bot a8d438d8f7 chore(version): Update package versions [skip ci] 2025-05-26 23:13:44 +00:00
ohif-bot fec45860b8 chore(version): version.json [skip ci] 2025-05-26 23:13:38 +00:00
Alireza b9ca76ea67 fix: e2e playwright tests runnigns (#5070) 2025-05-26 19:09:26 -04:00
ohif-bot ffc7d2bd6b chore(version): Update package versions [skip ci] 2025-05-26 20:12:50 +00:00
ohif-bot 1d91871d2c chore(version): version.json [skip ci] 2025-05-26 20:12:43 +00:00
Joe Boccanfuso ebfa1ff5f2 fix(capture): Disable or enable the tool group based on the show flag. (#5069) 2025-05-26 16:08:17 -04:00
Joe Boccanfuso 1873a8405f fix(frame-view): After pan and/or zoom, stack scroll is now synchronized for frame view (#5067) 2025-05-26 16:08:01 -04:00
ohif-bot df35cb55b6 chore(version): Update package versions [skip ci] 2025-05-26 04:09:21 +00:00
ohif-bot 9247d07ccd chore(version): version.json [skip ci] 2025-05-26 04:09:15 +00:00
Joe Boccanfuso 14f5f0c074 fix(router): Expose the react router from the worklist too. (#5062) 2025-05-26 00:04:16 -04:00
ohif-bot c2cc0832f3 chore(version): Update package versions [skip ci] 2025-05-24 02:16:33 +00:00
ohif-bot 10d64322bf chore(version): version.json [skip ci] 2025-05-24 02:16:27 +00:00
Joe Boccanfuso cb814e6a60 fix(context-menu): Move the reposition logic from the context menu to the dialog component. (#5060) 2025-05-23 22:12:08 -04:00
ohif-bot fa9bbe3afc chore(version): Update package versions [skip ci] 2025-05-16 18:20:40 +00:00
ohif-bot 7f2f739b1b chore(version): version.json [skip ci] 2025-05-16 18:20:34 +00:00
Alexis jankowski dcde841839 fix(cli): removing missing imports in mode template (#5049) 2025-05-16 14:11:01 -04:00
ohif-bot c784b7e6d5 chore(version): Update package versions [skip ci] 2025-05-16 17:53:12 +00:00
ohif-bot 0d8f43fa30 chore(version): version.json [skip ci] 2025-05-16 17:53:05 +00:00
Alireza 9b1b501d83 fix: backward compatibility with toolbar service (#5052) 2025-05-16 13:42:35 -04:00
Joe Boccanfuso c6b28654b5 fix(segmentation): segmentation stats calculations were not being done on a subsequent navigation to a mode (#5046) 2025-05-16 13:33:57 -04:00
ohif-bot fb1c2673a8 chore(version): Update package versions [skip ci] 2025-05-16 17:32:07 +00:00
ohif-bot b6b06ed09e chore(version): version.json [skip ci] 2025-05-16 17:32:01 +00:00
Joe Boccanfuso 338bd865c3 feat(viewport): Added getter for presentation id provider. (#5053) 2025-05-16 13:22:09 -04:00
ohif-bot cf1018c779 chore(version): Update package versions [skip ci] 2025-05-15 21:22:31 +00:00
ohif-bot 0f12fa16bc chore(version): version.json [skip ci] 2025-05-15 21:22:25 +00:00
Dan Rukasandsedghi 04ab79f8fb ui(feat): Adds light/dark mode support for viewport overlay text and markers (#5041)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-05-15 17:12:53 -04:00
ohif-bot 1fd39626e7 chore(version): Update package versions [skip ci] 2025-05-15 16:18:22 +00:00
ohif-bot 5e406a8c93 chore(version): version.json [skip ci] 2025-05-15 16:18:15 +00:00
Alireza 7c15bb8901 feat(actions): simplify action corner api through the toolbarService (#5033) 2025-05-15 12:08:12 -04:00
ohif-bot f71b88e639 chore(version): Update package versions [skip ci] 2025-05-13 15:29:21 +00:00
ohif-bot faaaccdfa7 chore(version): version.json [skip ci] 2025-05-13 15:29:14 +00:00
Celian-abd da51e6e3c1 fix: Added missing export for LayoutSelector in ui-next (#5040) 2025-05-13 11:24:25 -04:00
ohif-bot 4a91dfdfa5 chore(version): Update package versions [skip ci] 2025-05-12 13:39:55 +00:00
ohif-bot c2534198ad chore(version): version.json [skip ci] 2025-05-12 13:39:48 +00:00
Dan 950453fd70 fix(NotFoundStudy): Fixed the viewer not-found page when showStudyList: false (#5035) 2025-05-12 09:35:33 -04:00
ohif-bot f4ba9b3cc6 chore(version): Update package versions [skip ci] 2025-05-09 12:24:02 +00:00
ohif-bot 7610b4c6a5 chore(version): version.json [skip ci] 2025-05-09 12:23:55 +00:00
taliabeckie 349306da40 fix(DicomWebProxyDataSource): bug fix to save Segmentation files back to Kheops (#5028) 2025-05-09 08:19:20 -04:00
ohif-bot 66322c68a2 chore(version): Update package versions [skip ci] 2025-05-08 16:56:20 +00:00
ohif-bot 4e03f34fd4 chore(version): version.json [skip ci] 2025-05-08 16:56:14 +00:00
Alireza 1445e12124 feat(app): Decouple colorbar UI from service and enable action menu programatic control (#5018) 2025-05-08 12:51:30 -04:00
ohif-bot f14d9ebdd7 chore(version): Update package versions [skip ci] 2025-05-08 16:49:34 +00:00
ohif-bot a3bf972177 chore(version): version.json [skip ci] 2025-05-08 16:49:26 +00:00
Bill WallaceandMartin Bellehumeur, M. Eng. 578c3939f0 feat(thumbnails): Enhance DICOM Thumbnail Rendering with different strategies (#5004)
Co-authored-by: Martin Bellehumeur, M. Eng. <23396581+mbellehumeur@users.noreply.github.com>
2025-05-08 12:44:56 -04:00
ohif-bot dee6909a99 chore(version): Update package versions [skip ci] 2025-05-07 21:41:38 +00:00
ohif-bot 12094cc77a chore(version): version.json [skip ci] 2025-05-07 21:41:32 +00:00
Alireza a5ea3e15c7 fix(orientationMarker): MPR orientation change should get reflected too (#5020) 2025-05-07 17:37:12 -04:00
ohif-bot f412dc254b chore(version): Update package versions [skip ci] 2025-05-07 20:24:55 +00:00
ohif-bot c0ed128fb7 chore(version): version.json [skip ci] 2025-05-07 20:24:49 +00:00
Rafael Osipov c829270c80 feat: add locale files for Russian language, translated texts in locale json files to Russian language. (#4984) 2025-05-07 16:20:10 -04:00
ohif-bot a16d612a8a chore(version): Update package versions [skip ci] 2025-05-06 15:10:20 +00:00
ohif-bot 5f94ebe81a chore(version): version.json [skip ci] 2025-05-06 15:10:13 +00:00
Ibrahim 8218a9d034 feat(i18n): add Arabic translations (#5013) 2025-05-06 11:05:34 -04:00
ohif-bot 4b94f18f7c chore(version): Update package versions [skip ci] 2025-05-05 20:40:11 +00:00
ohif-bot 8e7e9ca02f chore(version): version.json [skip ci] 2025-05-05 20:40:04 +00:00
Alireza cd4e6408bc feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007) 2025-05-05 16:35:05 -04:00
ohif-bot c1fced0573 chore(version): Update package versions [skip ci] 2025-05-05 15:32:36 +00:00
ohif-bot e41019f2b3 chore(version): version.json [skip ci] 2025-05-05 15:32:30 +00:00
Alireza 383f45823c refactor(viewport): Remove resize detector and optimize viewport resizing logic (#5006) 2025-05-05 11:27:26 -04:00
ohif-bot 6337d2e9cb chore(version): Update package versions [skip ci] 2025-05-02 18:50:57 +00:00
ohif-bot e987710908 chore(version): version.json [skip ci] 2025-05-02 18:50:50 +00:00
Pedro Köhler 7dbe5e37b0 feat: improve menu modals customizations (#5011) 2025-05-02 14:45:39 -04:00
ohif-bot ff974e2a03 chore(version): Update package versions [skip ci] 2025-05-02 18:10:04 +00:00
ohif-bot cccf74d6ea chore(version): version.json [skip ci] 2025-05-02 18:09:57 +00:00
Alireza 4be8495335 fix/remove legacy ui (#5010) 2025-05-02 14:05:40 -04:00
ohif-bot 1cb8d328a4 chore(version): Update package versions [skip ci] 2025-05-01 22:22:48 +00:00
ohif-bot 2c8dda01ae chore(version): version.json [skip ci] 2025-05-01 22:22:42 +00:00
Alireza 4e185e4b21 docs: add llm txt (#5009) 2025-05-01 18:13:21 -04:00
ohif-bot 4732e8093e chore(version): Update package versions [skip ci] 2025-05-01 18:51:19 +00:00
ohif-bot 984471e972 chore(version): version.json [skip ci] 2025-05-01 18:51:13 +00:00
Julia Young 25cb71b3f6 docs: clarify terminology for study, series, and display sets (#5000) 2025-05-01 14:46:21 -04:00
ohif-bot 0e75f6d62c chore(version): Update package versions [skip ci] 2025-04-30 16:29:06 +00:00
ohif-bot d75b3e25b6 chore(version): version.json [skip ci] 2025-04-30 16:28:59 +00:00
Tang Cheng 0bb9b17636 Fix: issue #4759 Enable proper DICOM image rendering when Rescale parameters are set to null (#5003) 2025-04-30 12:24:06 -04:00
ohif-bot b21ee9bc5b chore(version): Update package versions [skip ci] 2025-04-29 18:58:50 +00:00
ohif-bot 7ed0f6d53b chore(version): version.json [skip ci] 2025-04-29 18:58:44 +00:00
Alireza 98f8187dfe fix(referencelines): and bring back the notification for worker updates but disallow duplication (#5005) 2025-04-29 14:53:37 -04:00
ohif-bot 4643d6dc40 chore(version): Update package versions [skip ci] 2025-04-29 17:02:30 +00:00
ohif-bot 258b353269 chore(version): version.json [skip ci] 2025-04-29 17:02:23 +00:00
devin-ai-integration[bot]andAlireza Sedghi 0f39496290 fix(template): Update mode template to match current implementation (OHI-1833) (#4998)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
2025-04-29 12:57:24 -04:00
ohif-bot 325cb3f7d5 chore(version): Update package versions [skip ci] 2025-04-22 17:33:27 +00:00
ohif-bot 088b563bf6 chore(version): version.json [skip ci] 2025-04-22 17:33:21 +00:00
Dan Rukas 09ac43f297 fix(docs): Fix SSR build errors by dynamically importing UI components and resolving other issues (#4991) 2025-04-22 13:28:49 -04:00
ohif-bot d59828ab22 chore(version): Update package versions [skip ci] 2025-04-22 14:50:19 +00:00
ohif-bot 5f04b0f3be chore(version): version.json [skip ci] 2025-04-22 14:50:12 +00:00
Alireza bdb847a6eb fix docs (#4988) 2025-04-22 10:45:14 -04:00
ohif-bot 0052bf1ae3 chore(version): Update package versions [skip ci] 2025-04-22 14:05:34 +00:00
ohif-bot 41815ee2af chore(version): version.json [skip ci] 2025-04-22 14:05:28 +00:00
Alireza 56fdb82812 fix docs (#4986) 2025-04-22 10:00:40 -04:00
ohif-bot fcde5ddbce chore(version): Update package versions [skip ci] 2025-04-22 13:20:21 +00:00
ohif-bot 7818f406d8 chore(version): version.json [skip ci] 2025-04-22 13:20:15 +00:00
Dan Rukas 329d5b6237 docs(components): Adds new components to ui-next documentation (#4974) 2025-04-22 09:15:40 -04:00
Devu-trenser 3b8faa8a76 feat: Improve unsupported displayset messages (#4979) 2025-04-22 09:11:28 -04:00
ohif-bot 59136e5435 chore(version): Update package versions [skip ci] 2025-04-18 15:38:25 +00:00
ohif-bot 88fccda491 chore(version): version.json [skip ci] 2025-04-18 15:38:19 +00:00
Pedro Köhler 5688a2b4b8 fix(dicomWebClient): use public getter for activeDataSourceName instead of accessing private property (#4973) 2025-04-18 11:33:42 -04:00
ohif-bot f02ce22ae3 chore(version): Update package versions [skip ci] 2025-04-17 20:49:39 +00:00
ohif-bot ff5a152270 chore(version): version.json [skip ci] 2025-04-17 20:49:33 +00:00
dependabot[bot] 082014a694 chore(deps): bump estree-util-value-to-estree from 3.2.1 to 3.3.3 in /platform/docs (#4929)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 16:43:55 -04:00
dependabot[bot] 194b4971c2 chore(deps): bump @babel/runtime from 7.26.0 to 7.27.0 in /platform/docs (#4964)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 16:43:48 -04:00
dependabot[bot] d6c7b9c23c chore(deps): bump http-proxy-middleware from 2.0.7 to 2.0.9 (#4963)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 16:43:39 -04:00
dependabot[bot] b82465344c chore(deps): bump @babel/runtime-corejs3 from 7.24.7 to 7.27.0 (#4962)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 16:43:30 -04:00
ohif-bot f8cb810b92 chore(version): Update package versions [skip ci] 2025-04-17 12:36:52 +00:00
ohif-bot dbda78df15 chore(version): version.json [skip ci] 2025-04-17 12:36:45 +00:00
Pedro Köhler 66a16cd22f fix(ExtensionManager): getDataSources method (#4959) 2025-04-17 08:31:12 -04:00
ohif-bot 5a9fcd4bf0 chore(version): Update package versions [skip ci] 2025-04-16 21:40:06 +00:00
ohif-bot 69cddff628 chore(version): version.json [skip ci] 2025-04-16 21:40:00 +00:00
Alireza 08c14e9ba2 fix/runner (#4971) 2025-04-16 17:35:32 -04:00
Alireza 7fd522ab68 fix/runner (#4970) 2025-04-16 16:57:59 -04:00
Alireza df74557866 fix/runner (#4969) 2025-04-16 16:49:36 -04:00
Alireza 69063a41b7 fix/runner (#4968) 2025-04-16 16:41:35 -04:00
Alireza 41fb597085 fix/runner4 (#4967) 2025-04-16 16:31:24 -04:00
ohif-bot a3a35471a9 chore(version): Update package versions [skip ci] 2025-04-16 20:21:17 +00:00
ohif-bot a0bcac15cd chore(version): version.json [skip ci] 2025-04-16 20:21:10 +00:00
Alireza ac7875128c fix/runner (#4966) 2025-04-16 16:16:28 -04:00
Alireza bd02205ef9 fix/runner (#4965) 2025-04-16 16:09:06 -04:00
Alireza 9535422361 fix: github runner (#4961) 2025-04-16 16:02:06 -04:00
ohif-bot a13380407b chore(version): Update package versions [skip ci] 2025-04-16 20:01:50 +00:00
ohif-bot 1a0d7a930c chore(version): version.json [skip ci] 2025-04-16 20:01:43 +00:00
Alireza 91117f40f8 chore: add test coverage (#4960) 2025-04-16 15:56:52 -04:00
ohif-bot a41ed3bade chore(version): Update package versions [skip ci] 2025-04-14 14:05:04 +00:00
ohif-bot f12f2bfec5 chore(version): version.json [skip ci] 2025-04-14 14:04:58 +00:00
Alireza 4a87302f06 fix: making onnxruntime work with rsbuild (#4957) 2025-04-14 09:59:39 -04:00
ohif-bot 5dae147155 chore(version): Update package versions [skip ci] 2025-04-11 19:09:38 +00:00
ohif-bot 4952dab010 chore(version): version.json [skip ci] 2025-04-11 19:09:31 +00:00
Dan Rukas 5425308e55 ui(fix): Study panel scrollbar fix and updates (#4951) 2025-04-11 15:03:56 -04:00
ohif-bot c50298f394 chore(version): Update package versions [skip ci] 2025-04-11 16:46:37 +00:00
ohif-bot a82ebeebb2 chore(version): version.json [skip ci] 2025-04-11 16:46:31 +00:00
Pedro Köhler ed94df0dfe fix: add back study browser scrollbar (#4942) 2025-04-11 12:41:16 -04:00
Pedro Köhler 4926a9532c feat: expose scroll thumbnail into view (#4941) 2025-04-11 12:41:04 -04:00
Pedro Köhler 7a36eba67c fix: enable measurements panel scroll (#4943) 2025-04-11 12:40:35 -04:00
ohif-bot 8a08d26f01 chore(version): Update package versions [skip ci] 2025-04-11 13:47:41 +00:00
ohif-bot de598cdc1f chore(version): version.json [skip ci] 2025-04-11 13:47:35 +00:00
Pedro Köhler a9b180c5d4 fix: make datasource selection work (#4944) 2025-04-11 09:42:54 -04:00
ohif-bot ffc7ab6845 chore(version): Update package versions [skip ci] 2025-04-10 03:41:05 +00:00
ohif-bot 1714aa8ec7 chore(version): version.json [skip ci] 2025-04-10 03:40:59 +00:00
Alireza ce0d152f92 docs: next version (#4940) 2025-04-09 23:36:24 -04:00
ohif-bot 16bcc06f15 chore(version): Update package versions [skip ci] 2025-04-10 03:30:26 +00:00
ohif-bot bb42fd2a8d chore(version): version.json [skip ci] 2025-04-10 03:30:20 +00:00
Alireza e37168094a chore(version): move to the next beta version [BUMP BETA] (#4939) 2025-04-09 23:25:30 -04:00
ohif-bot e1e9b5ccb1 chore(version): Update package versions [skip ci] 2025-04-09 20:54:34 +00:00
ohif-bot 6b7e73b78a chore(version): version.json [skip ci] 2025-04-09 20:54:27 +00:00
AlirezaandJoe Boccanfuso 43bc142439 fix(ui): Enhance UI Components and Update CornerstoneJS Dependencies and resizable panels (#4938)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2025-04-09 16:49:55 -04:00
ohif-bot c05ec1909f chore(version): Update package versions [skip ci] 2025-04-09 14:00:54 +00:00
ohif-bot c7529394ad chore(version): version.json [skip ci] 2025-04-09 14:00:48 +00:00
Pedro Köhler f6f60a63d2 fix: x-ray pixel spacing calibration (#4922) 2025-04-09 09:55:51 -04:00
ohif-bot 0d2ccc5830 chore(version): Update package versions [skip ci] 2025-04-09 03:01:36 +00:00
ohif-bot 08be5cc656 chore(version): version.json [skip ci] 2025-04-09 03:01:30 +00:00
Alireza c453c29449 docs: migration guides 3p10 (#4937) 2025-04-08 22:57:07 -04:00
ohif-bot a69dd8b1a0 chore(version): Update package versions [skip ci] 2025-04-08 22:38:59 +00:00
ohif-bot 17c5642a59 chore(version): version.json [skip ci] 2025-04-08 22:38:53 +00:00
Alireza bd2f843469 chore: update screenshopts for test (#4934) 2025-04-08 18:34:45 -04:00
ohif-bot 8b9b02d84c chore(version): Update package versions [skip ci] 2025-04-08 21:25:58 +00:00
ohif-bot c9958bafad chore(version): version.json [skip ci] 2025-04-08 21:25:52 +00:00
Dan Rukas 85f465968b ui(fix): small Create Report dialog fixes (#4935) 2025-04-08 17:21:08 -04:00
ohif-bot 47e491f1c8 chore(version): Update package versions [skip ci] 2025-04-08 16:10:38 +00:00
ohif-bot e56b53e897 chore(version): version.json [skip ci] 2025-04-08 16:10:32 +00:00
AlirezaandDan Rukas 2bdf4afa4d refactor(ui): Migrate components to ui-next (#4930)
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2025-04-08 12:06:07 -04:00
Dan Rukas 1b8729dded ui(components): CinePlayer updated for ui-next (#4932) 2025-04-08 12:03:35 -04:00
ohif-bot f71d370969 chore(version): Update package versions [skip ci] 2025-04-08 13:48:32 +00:00
ohif-bot 033831ee72 chore(version): version.json [skip ci] 2025-04-08 13:48:24 +00:00
Dan Rukas 49c08298fc ui(colors): Adds neutral color variable and updates scrollbars (#4933) 2025-04-08 09:43:30 -04:00
elawendeandErnst Lawende cfa202e89a fix(OIDC): Use new access_token after refresh (#4853)
Co-authored-by: Ernst Lawende <ernstl@bastion365.nl>
2025-04-08 09:43:06 -04:00
ohif-bot 391a5f3bdd chore(version): Update package versions [skip ci] 2025-04-07 20:50:11 +00:00
ohif-bot 64d65a6830 chore(version): version.json [skip ci] 2025-04-07 20:50:05 +00:00
Alireza 3653de7487 fix(segmentation): Add segment jump for new segments and make panels scrollable (#4928) 2025-04-07 16:45:29 -04:00
Dan Rukas a8ac297e16 ui(components): Viewport components updated to ui-next with design updates (#4886) 2025-04-07 16:45:05 -04:00
ohif-bot 4e09f85d5d chore(version): Update package versions [skip ci] 2025-04-07 14:09:45 +00:00
ohif-bot d2f187153d chore(version): version.json [skip ci] 2025-04-07 14:09:39 +00:00
Pedro Köhler 62e5a62066 feat: allow saving to same SR series (#4889) 2025-04-07 10:05:22 -04:00
ohif-bot 7b40963997 chore(version): Update package versions [skip ci] 2025-04-04 17:38:08 +00:00
ohif-bot 75493334cf chore(version): version.json [skip ci] 2025-04-04 17:38:00 +00:00
Pedro Köhler 746f2524eb fix(cine): Resolve Cine Player Faults on DisplaySet Update (#4909) 2025-04-04 13:33:36 -04:00
ohif-bot 0eb9ff235c chore(version): Update package versions [skip ci] 2025-04-04 16:16:04 +00:00
ohif-bot e9b95afd4c chore(version): version.json [skip ci] 2025-04-04 16:15:57 +00:00
Alireza fc33bd0d59 feat(image capture): Enhance SEG and RTSS Support with Dependency Updates and Bug Fixes (#4924) 2025-04-04 12:11:29 -04:00
ohif-bot 479e280fdf chore(version): Update package versions [skip ci] 2025-04-04 16:07:02 +00:00
ohif-bot d4f81cd2b7 chore(version): version.json [skip ci] 2025-04-04 16:06:55 +00:00
nithin-trenserandDevu Jayalekshmi 54fded554e feat: Automate tracking prompts for a less disruptive annotation workflow (#4822)
Co-authored-by: Devu Jayalekshmi <devu.jayalekshmi@trenser.com>
2025-04-04 12:02:13 -04:00
ohif-bot c0391cea1b chore(version): Update package versions [skip ci] 2025-04-03 15:45:43 +00:00
ohif-bot a9e69eb36f chore(version): version.json [skip ci] 2025-04-03 15:45:36 +00:00
dependabot[bot] a52be6460b chore(deps): bump @babel/runtime-corejs2 from 7.25.7 to 7.26.10 (#4843)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:33:33 -04:00
dependabot[bot] 587b6423cb chore(deps): bump @babel/helpers from 7.26.0 to 7.26.10 in /platform/docs (#4861)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:33:19 -04:00
dependabot[bot] f52a32d6c5 chore(deps): bump @babel/runtime from 7.24.7 to 7.26.10 (#4860)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:33:09 -04:00
dependabot[bot] 8142c55b6a chore(deps): bump @babel/runtime-corejs3 from 7.26.0 to 7.26.10 in /platform/docs (#4859)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:32:54 -04:00
dependabot[bot] 14afb42e2c chore(deps): bump axios from 0.27.2 to 1.8.4 (#4905)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:32:45 -04:00
dependabot[bot] 4e8e4f1491 chore(deps): bump tar-fs from 2.1.1 to 2.1.2 (#4901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:32:39 -04:00
dependabot[bot] 814a2c914b chore(deps): bump image-size from 1.2.0 to 1.2.1 in /platform/docs (#4912)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 11:32:31 -04:00
ohif-bot f4fb42adb2 chore(version): Update package versions [skip ci] 2025-04-02 22:23:12 +00:00
ohif-bot 0fee0236c2 chore(version): version.json [skip ci] 2025-04-02 22:23:05 +00:00
Alireza e7fe879318 chore: update dependabot (#4916) 2025-04-02 18:17:05 -04:00
ohif-bot a619634678 chore(version): Update package versions [skip ci] 2025-04-02 22:15:26 +00:00
ohif-bot 651a7dc574 chore(version): version.json [skip ci] 2025-04-02 22:15:20 +00:00
Alireza 8432d5f633 fix(seg): Enhance segmentation tools and UI, refactor code, and update dependencies (#4915) 2025-04-02 18:10:47 -04:00
ohif-bot 1dc00d3030 chore(version): Update package versions [skip ci] 2025-04-01 18:23:53 +00:00
ohif-bot 44d450c5b8 chore(version): version.json [skip ci] 2025-04-01 18:23:46 +00:00
Alireza c75a3f3af5 feat(segmentation): Enhance Segmentation with New AI and Once Click Tools (#4910) 2025-04-01 14:19:03 -04:00
ohif-bot 908482d2e0 chore(version): Update package versions [skip ci] 2025-04-01 13:35:34 +00:00
ohif-bot 6b26db387e chore(version): version.json [skip ci] 2025-04-01 13:35:27 +00:00
Dan Rukasandsedghi b7419fb6a1 ui(components): Adds InputNumber component and 4d panel updates (#4874)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2025-04-01 09:30:32 -04:00
ohif-bot c4045251fb chore(version): Update package versions [skip ci] 2025-03-31 13:14:52 +00:00
ohif-bot ec4cc64b06 chore(version): version.json [skip ci] 2025-03-31 13:14:46 +00:00
dxlin d9473e8db2 test: update Test/tmtv for v3.10.0-beta.136 (#4893) 2025-03-31 09:10:31 -04:00
ohif-bot 7f3fb9bb61 chore(version): Update package versions [skip ci] 2025-03-28 14:57:45 +00:00
ohif-bot 8cf5b52b84 chore(version): version.json [skip ci] 2025-03-28 14:57:39 +00:00
Alireza d7bd36544a fix: jump to measurement forever (#4867) 2025-03-28 10:53:04 -04:00
Alireza eafa2efda5 Revert "fix(measurement): fix for measurement jump is not switching series in 2DMPR (#4779)" (#4898) 2025-03-28 10:17:33 -04:00
arul-trenser 3f06c966eb fix(measurement): fix for measurement jump is not switching series in 2DMPR (#4779) 2025-03-28 10:14:19 -04:00
ohif-bot 951f95ef0a chore(version): Update package versions [skip ci] 2025-03-27 20:51:50 +00:00
ohif-bot 5737935a96 chore(version): version.json [skip ci] 2025-03-27 20:51:44 +00:00
Bill Wallace 459b0d223a fix: routerBasename setting (#4891) 2025-03-27 16:47:16 -04:00
ohif-bot c368d763b0 chore(version): Update package versions [skip ci] 2025-03-27 19:58:04 +00:00
ohif-bot 6b6f49b867 chore(version): version.json [skip ci] 2025-03-27 19:57:58 +00:00
Igor Octaviano 892d03fc3c fix(seg): multiframe SEG (#4890) 2025-03-27 15:52:22 -04:00
ohif-bot 2acc878f26 chore(version): Update package versions [skip ci] 2025-03-26 18:02:55 +00:00
ohif-bot 1df91fd104 chore(version): version.json [skip ci] 2025-03-26 18:02:48 +00:00
Pedro Köhler 9bde644fce fix: remove duplicated deleteMeasurements code (#4895) 2025-03-26 13:58:15 -04:00
ohif-bot 7f8fa1e1f4 chore(version): Update package versions [skip ci] 2025-03-26 17:44:41 +00:00
ohif-bot dce9c6b7c3 chore(version): version.json [skip ci] 2025-03-26 17:44:35 +00:00
Pedro Köhler 467acbd997 fix: datasource connection error style (#4888) 2025-03-26 13:40:09 -04:00
ohif-bot f90184d9d9 chore(version): Update package versions [skip ci] 2025-03-25 15:04:03 +00:00
ohif-bot cc9720dca2 chore(version): version.json [skip ci] 2025-03-25 15:03:57 +00:00
Alireza 4dd485bd2d fix(seg): orthanc seg (#4883) 2025-03-25 10:59:19 -04:00
ohif-bot 209d81f475 chore(version): Update package versions [skip ci] 2025-03-25 13:37:07 +00:00
ohif-bot 5c319ce4f0 chore(version): version.json [skip ci] 2025-03-25 13:37:00 +00:00
Bill Wallace f6fa509874 fix: Skip showing NaN for average pixel/max value (#4884) 2025-03-25 09:32:36 -04:00
Bill Wallace 8e47112d8e fix: Delete study measurements instead of all measurements (#4882) 2025-03-25 09:32:29 -04:00
Pedro Köhler 165c9dc710 fix: compare series date time (#4887) 2025-03-25 09:31:49 -04:00
ohif-bot 8faa9bf851 chore(version): Update package versions [skip ci] 2025-03-20 19:18:39 +00:00
ohif-bot 246c10da07 chore(version): version.json [skip ci] 2025-03-20 19:18:32 +00:00
Dan Rukas 782a6a0c88 ui(panels): Tool option components and responsive behavior for ROIThreshold (#4876) 2025-03-20 15:13:59 -04:00
ohif-bot 5293253ecd chore(version): Update package versions [skip ci] 2025-03-20 16:10:51 +00:00
ohif-bot 7f86ca1a2b chore(version): version.json [skip ci] 2025-03-20 16:10:45 +00:00
Pedro Köhler 01c3726ced feat: add customization to PanelStudyBrowser dbl click callback (#4871) 2025-03-20 12:06:26 -04:00
ohif-bot f73c485821 chore(version): Update package versions [skip ci] 2025-03-20 13:26:51 +00:00
ohif-bot 30a3965da7 chore(version): version.json [skip ci] 2025-03-20 13:26:45 +00:00
Bill Wallace df8efba82c feat: Group findings table values with custom components/grouping functions (#4712) 2025-03-20 09:22:45 -04:00
ohif-bot 0522d58d9b chore(version): Update package versions [skip ci] 2025-03-19 22:29:06 +00:00
ohif-bot 576ad47ebe chore(version): version.json [skip ci] 2025-03-19 22:29:00 +00:00
Alireza 5f112337b6 feat(segmentation): Enhance Segmentation Tools with Preview and Selection Features (#4870) 2025-03-19 18:24:33 -04:00
ohif-bot 20042f9109 chore(version): Update package versions [skip ci] 2025-03-19 15:57:55 +00:00
ohif-bot d59b9d88fa chore(version): version.json [skip ci] 2025-03-19 15:57:48 +00:00
Sai Shanmukha Narumanchi 7f229d5c23 fix(DicomWebDataSource) : Fix a missing null check in addRetrieveBulkDataNaturalized (#4866) 2025-03-19 11:52:56 -04:00
ohif-bot eac0308275 chore(version): Update package versions [skip ci] 2025-03-18 21:11:23 +00:00
ohif-bot c2f9252aef chore(version): version.json [skip ci] 2025-03-18 21:11:16 +00:00
Pedro Köhler 55d6393a3f feat: overlapping segments (#4849) 2025-03-18 17:07:13 -04:00
1701 changed files with 83715 additions and 31817 deletions

No files matched your search

+9 -6
View File
@@ -242,7 +242,7 @@ jobs:
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build Docker image for arm64
name: Build Docker image for arm64 (Release)
command: |
# This file will exist if a new version was published by
# our command in the previous job.
@@ -339,7 +339,6 @@ jobs:
resource_class: large
parallelism: 8
steps:
- install_bun
- run:
name: Install System Dependencies
command: |
@@ -353,12 +352,11 @@ jobs:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
install-command: bun install --no-save
package-manager: yarn
install-command: yarn install --no-save
- cypress/run-tests:
cypress-command: |
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record --parallel
start-command: bun run test:data && bun run test:e2e:serve
start-command: yarn run test:data && yarn run test:e2e:serve
DOCKER_MULTIARCH_MANIFEST:
<<: *defaults
@@ -369,7 +367,7 @@ jobs:
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Create and push multi-architecture manifest
name: Create and push multi-architecture manifest (Release)
command: |
# This file will exist if a new version was published by
# our command in the previous job.
@@ -457,8 +455,13 @@ workflows:
filters:
branches:
only: master
# - HOLD_FOR_APPROVAL:
# type: approval
# requires:
# - BUILD
- NPM_PUBLISH:
requires:
# - HOLD_FOR_APPROVAL
- BUILD
- DOCKER_BETA_PUBLISH:
requires:
+1 -1
View File
@@ -7,7 +7,7 @@ server {
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
try_files $uri $uri/ ${PUBLIC_URL}index.html;
add_header Cross-Origin-Resource-Policy same-origin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
+2 -3
View File
@@ -2,12 +2,11 @@
if [ -n "$SSL_PORT" ]
then
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template | envsubst '${PUBLIC_URL}' > /etc/nginx/conf.d/default.conf
else
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
envsubst '${PORT}:${PUBLIC_URL}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
fi
if [ -n "$APP_CONFIG" ]; then
echo "$APP_CONFIG" > /usr/share/nginx/html${PUBLIC_URL}app-config.js
echo "Using custom APP_CONFIG environment variable"
+12
View File
@@ -1,7 +1,19 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: 'bun'
directory: '/'
schedule:
interval: 'daily'
labels: ['dependencies']
commit-message:
prefix: 'chore'
include: 'scope'
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
labels: ['dependencies']
commit-message:
prefix: 'chore'
include: 'scope'
+108
View File
@@ -0,0 +1,108 @@
name: Build and Deploy Docs
on:
push:
branches: [master]
env:
ACTIONS_STEP_DEBUG: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-deploy-docs:
timeout-minutes: 60
runs-on: ubuntu-latest
# Need permissions to read actions and pull requests
permissions:
actions: read
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 20 # Or your desired Node version
- name: Install root dependencies
run: bun install --frozen-lockfile
# Removed Playwright tests and coverage generation steps
- name: Find PR and associated workflow run
id: find_pr_run
env:
GH_TOKEN: ${{ github.token }}
MERGE_COMMIT_SHA: ${{ github.sha }}
run: |
# Find the PR associated with the merge commit SHA
# Note: This relies on the merge commit being directly pushed to main
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
if [ -z "$PR_DATA" ]; then
echo "Could not find merged PR for commit $MERGE_COMMIT_SHA."
# Decide how to handle - fail, or maybe generate coverage now?
# For now, let's fail.
exit 1
fi
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
echo "Found PR Number: $PR_NUMBER"
echo "Found PR Head SHA: $PR_HEAD_SHA"
# Find the latest workflow run ID for the playwright workflow on the PR head commit
# Uses the workflow file name 'playwright.yml'
# Remove the --status success flag to find any run
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then
echo "Could not find any 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA)."
# Decide how to handle - maybe try finding the artifact from the merge commit run if that exists?
# For now, let's fail.
exit 1
fi
echo "Found Run ID: $RUN_ID"
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
- name: Download coverage artifact from PR run
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p ./coverage-artifact
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
# Check if download was successful (e.g., check if files exist)
if [ ! -f ./coverage-artifact/base.css ]; then
echo "Failed to download or find expected files in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
exit 1
fi
echo "Artifact downloaded successfully."
- name: Install docs dependencies
run: cd platform/docs && bun install
- name: Copy coverage to docs static directory
run: |
# Copy files from the downloaded artifact directory
mkdir -p platform/docs/static/coverage
cp -r ./coverage-artifact/* platform/docs/static/coverage/
# Copy specific asset files from the downloaded artifact root to static root
cp ./coverage-artifact/base.css platform/docs/static/
cp ./coverage-artifact/block-navigation.js platform/docs/static/
cp ./coverage-artifact/prettify.css platform/docs/static/
cp ./coverage-artifact/prettify.js platform/docs/static/
cp ./coverage-artifact/favicon.png platform/docs/static/
cp ./coverage-artifact/sort-arrow-sprite.png platform/docs/static/
cp ./coverage-artifact/sorter.js platform/docs/static/
- name: Build docs
run: cd platform/docs && bun run build
- name: Deploy to Netlify
run: |
cd platform/docs
npx netlify-cli deploy --dir=./build --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
+98
View File
@@ -0,0 +1,98 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
pull_request:
branches: [ "main" ]
schedule:
- cron: '15 1 * * 5'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+32 -54
View File
@@ -1,76 +1,54 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.50.0-noble
runs-on: self-hosted
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shardTotal: [10]
node-version: [20]
steps:
- uses: actions/checkout@v4
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Install Playwright browsers
run: npx playwright install
- name: Run Playwright tests
run:
export NODE_OPTIONS="--max_old_space_size=8192" && bun x playwright test --shard=${{
matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts
run: |
export NODE_OPTIONS="--max_old_space_size=10192"
bun run test:e2e:coverage
- name: Create directory of test results
if: ${{ !cancelled() }}
run: |
mkdir -p packaged-test-results
cp -r ./tests/test-results packaged-test-results/ || true
cp ./tests/playwright-report.json packaged-test-results/ || true
- name: Upload directory of test results artifact
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [playwright-tests]
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.1-focal
steps:
- name: Install unzip
run: apt-get update && apt-get install -y unzip
- uses: oven-sh/setup-bun@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Set up Git safe directory
run: git config --global --add safe.directory /__w/Viewers/Viewers
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: bun x playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
name: playwright-results
path: packaged-test-results/
retention-days: 5
- name: create the coverage report
run: |
bun nyc report --reporter=lcov --reporter=text
- name: Upload the coverage report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
name: coverage-report-pr
path: coverage
retention-days: 3
+7 -1
View File
@@ -1,7 +1,7 @@
# Packages
node_modules
.cursor/
.nyc_output/
# Output
build
dist
@@ -13,6 +13,7 @@ coverage/
.yarn/
.nx/
addOns/yarn.lock
playwright-report/
# YALC (for Erik)
.yalc
@@ -57,3 +58,8 @@ tests/test-results/
tests/playwright-report/
/blob-report/
/playwright/.cache/
**/.claude/settings.local.json
# Backup files
*~
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"instrument": true,
"sourceMap": true,
"cache": false,
"all": true,
"include": [
"platform/*/src/**/*.ts",
"platform/*/src/**/*.js",
"extensions/*/src/**/*.ts",
"extensions/*/src/**/*.js",
"modes/*/src/**/*.ts",
"modes/*/src/**/*.js"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/test/**",
"**/tests/**",
"**/examples/**",
"**/stories/**",
"platform/docs/**"
]
}
+4 -1
View File
@@ -82,6 +82,7 @@
"Reslices",
"roadmap",
"ROADMAPS",
"rtstruct",
"Segmentations",
"semibold",
"sitk",
@@ -109,6 +110,8 @@
"wado",
"wadors",
"wadouri",
"workerpool"
"workerpool",
"Colorbar",
"Colorbars"
]
}
+26 -8
View File
@@ -26,6 +26,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
const NODE_ENV = process.env.NODE_ENV;
const QUICK_BUILD = process.env.QUICK_BUILD;
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
const IS_COVERAGE = process.env.COVERAGE === 'true';
// read from ../version.txt
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
@@ -49,6 +50,7 @@ const defineValues = {
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
'process.env.TEST_ENV': JSON.stringify(process.env.TEST_ENV || ''),
};
// Only redefine updated values. This avoids warning messages in the logs
@@ -99,14 +101,30 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
...(isProdBuild
? []
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
...(IS_COVERAGE
? [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
plugins: ['istanbul'],
},
},
},
]
: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: isProdBuild ? [] : ['react-refresh/babel'],
},
},
]),
]),
{
test: /\.svg?$/,
+1568
View File
File diff suppressed because it is too large. Load diff
+7 -6
View File
@@ -62,9 +62,6 @@ RUN bun pm cache rm
RUN bun install
# Copy the local directory
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . .
# Do a second install to finalize things after the copy
RUN bun run show:config
RUN bun install
# Build here
# After install it should hopefully be stable until the local directory changes
@@ -72,6 +69,7 @@ ENV QUICK_BUILD true
# ENV GENERATE_SOURCEMAP=false
ARG APP_CONFIG=config/default.js
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
RUN bun run show:config
RUN bun run build
@@ -84,16 +82,19 @@ RUN ./.docker/compressDist.sh
# which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.27-alpine as final
#RUN apk add --no-cache bash
ARG PORT=80
ENV PORT=${PORT}
ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL}
ARG PORT=80
ENV PORT=${PORT}
RUN rm /etc/nginx/conf.d/default.conf
USER nginx
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
RUN chmod 777 /usr/src/entrypoint.sh
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html${PUBLIC_URL}
COPY --from=builder /usr/src/app/platform/app/dist/index.html /usr/share/nginx/html
# Copy paths that are renamed/redirected generally
# Microscopy libraries depend on root level include, so must be copied
COPY --from=builder /usr/src/app/platform/app/dist/dicom-microscopy-viewer /usr/share/nginx/html/dicom-microscopy-viewer
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
# The nginx user cannot chmod it, so change to root.
USER root
-1
View File
@@ -1,5 +1,4 @@
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<div align="center">
<h1>OHIF Medical Imaging Viewer</h1>
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
File diff suppressed because it is too large. Load diff
+13 -16
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"license": "MIT",
"private": true,
"engines": {
@@ -13,24 +13,24 @@
"@kitware/vtk.js": "32.12.0",
"clsx": "^2.1.1",
"core-js": "^3.2.1",
"moment": "^2.9.4"
"moment": "^2.30.1"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@babel/eslint-parser": "7.28.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@rsbuild/core": "^1.1.13",
"@rsbuild/plugin-node-polyfill": "1.2.0",
"@rsbuild/plugin-react": "^1.1.0",
"@rsbuild/core": "^1.3.16",
"@rsbuild/plugin-node-polyfill": "^1.3.0",
"@rsbuild/plugin-react": "^1.3.1",
"@svgr/webpack": "^8.1.0",
"@swc/helpers": "^0.5.15",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"autoprefixer": "^10.4.4",
"babel-eslint": "9.x",
"babel-loader": "^8.2.4",
"babel-plugin-module-resolver": "^5.0.0",
"clean-webpack-plugin": "^3.0.0",
@@ -39,14 +39,14 @@
"css-loader": "^6.8.1",
"dotenv": "^8.1.0",
"eslint": "^8.39.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-prettier": "7.2.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-flowtype": "^7.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
@@ -68,25 +68,22 @@
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"postcss-preset-env": "^7.4.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9",
"react-refresh": "^0.14.2",
"semver": "^7.5.1",
"serve": "^14.2.4",
"shader-loader": "^1.3.1",
"shx": "^0.3.3",
"source-map-loader": "^4.0.1",
"start-server-and-test": "^1.10.0",
"style-loader": "^1.0.0",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.1.4",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.94.0",
"webpack": "^5.94.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "4.7.3",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "5.2.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3",
"workbox-webpack-plugin": "^6.1.5",
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
+11 -11
View File
@@ -21,24 +21,24 @@
"preinstall": "cd .. && node preinstall.js"
},
"devDependencies": {
"@babel/core": "7.24.7",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-runtime": "7.24.7",
"@babel/plugin-transform-typescript": "^7.13.0",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.13.0"
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0",
"@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1"
},
"resolutions": {
"**/@babel/runtime": "^7.20.13",
"commander": "8.3.0",
"dcmjs": "0.38.0",
"dcmjs": "0.43.1",
"dicomweb-client": ">=0.10.4",
"nth-check": "^2.1.1",
"trim-newlines": "^5.0.0",
+8 -4
View File
@@ -3,10 +3,10 @@ module.exports = {
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
plugins: [
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-transform-class-properties', { loose: true }],
'@babel/plugin-transform-typescript',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-transform-private-property-in-object', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
'@babel/plugin-transform-class-static-block',
],
env: {
@@ -18,19 +18,23 @@ module.exports = {
{
modules: 'commonjs',
debug: false,
targets: { node: 'current' },
bugfixes: true,
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'babel-plugin-istanbul',
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/transform-destructuring',
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-class-static-block',
'@babel/plugin-transform-for-of',
],
},
production: {
+1354 -1081
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
fb93ef921999010cbabe4bc7d8bef9b90c8efc71
f1d158bbe81fd9f6052ce23c8588c9c93d76360c
+118
View File
@@ -0,0 +1,118 @@
diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
index 6c1e4f8f6..56dac15b5 100644
--- a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
+++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
@@ -156,65 +156,65 @@ function ViewportOrientationMenu({
>
<Button
variant="ghost"
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
onClick={() => handleOrientationChange('axial')}
>
- <span className="mr-0 flex w-4 items-center justify-center">
+ <div className="mr-1 flex w-6 items-center justify-start">
{currentOrientation === 'axial' ? (
- <Icons.Checked className="text-primary h-5 w-5" />
+ <Icons.Checked className="text-primary h-6 w-6" />
) : null}
- </span>
- <span className="flex-1 text-left">Axial</span>
+ </div>
+ <div className="flex-1 text-left">Axial</div>
</Button>
<Button
variant="ghost"
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
onClick={() => handleOrientationChange('sagittal')}
>
- <span className="mr-0 flex w-4 items-center justify-center">
+ <div className="mr-1 flex w-6 items-center justify-start">
{currentOrientation === 'sagittal' ? (
- <Icons.Checked className="text-primary h-5 w-5" />
+ <Icons.Checked className="text-primary h-6 w-6" />
) : null}
- </span>
- <span className="flex-1 text-left">Sagittal</span>
+ </div>
+ <div className="flex-1 text-left">Sagittal</div>
</Button>
<Button
variant="ghost"
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
onClick={() => handleOrientationChange('coronal')}
>
- <span className="mr-0 flex w-4 items-center justify-center">
+ <div className="mr-1 flex w-6 items-center justify-start">
{currentOrientation === 'coronal' ? (
- <Icons.Checked className="text-primary h-5 w-5" />
+ <Icons.Checked className="text-primary h-6 w-6" />
) : null}
- </span>
- <span className="flex-1 text-left">Coronal</span>
+ </div>
+ <div className="flex-1 text-left">Coronal</div>
</Button>
<Button
variant="ghost"
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
onClick={() => handleOrientationChange('acquisition')}
>
- <span className="mr-0 flex w-4 items-center justify-center">
+ <div className="mr-1 flex w-6 items-center justify-start">
{currentOrientation === 'acquisition' ? (
- <Icons.Checked className="text-primary h-5 w-5" />
+ <Icons.Checked className="text-primary h-6 w-6" />
) : null}
- </span>
- <span className="flex-1 text-left">Acquisition</span>
+ </div>
+ <div className="flex-1 text-left">Acquisition</div>
</Button>
{/* Divider */}
- <div className="my-2 border-t border-white/20" />
+ <div className="mx-1 my-2 border-t border-white/20" />
<Button
variant="ghost"
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
onClick={() => handleOrientationChange('reformat')}
>
- <span className="mr-0 flex w-4 items-center justify-center">
+ <div className="mr-1 flex w-6 items-center justify-start">
{currentOrientation === 'reformat' ? (
- <Icons.Checked className="text-primary h-5 w-5" />
+ <Icons.Checked className="text-primary h-6 w-6" />
) : null}
- </span>
- <span className="flex-1 text-left">Reformat</span>
+ </div>
+ <div className="flex-1 text-left">Reformat</div>
</Button>
</PopoverContent>
</Popover>
diff --git a/platform/ui-next/src/components/Icons/Sources/Checked.tsx b/platform/ui-next/src/components/Icons/Sources/Checked.tsx
index 83c8fc57e..c88b75e0d 100644
--- a/platform/ui-next/src/components/Icons/Sources/Checked.tsx
+++ b/platform/ui-next/src/components/Icons/Sources/Checked.tsx
@@ -4,14 +4,14 @@ import type { IconProps } from '../types';
export const Checked = (props: IconProps) => (
<svg
{...props}
- width="24"
+ width="20"
height="24"
- viewBox="0 0 24 24"
+ viewBox="0 0 20 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
- d="M16 9L11.3333 15L8 12.4737"
+ d="M14 9L9.3333 15L6 12.4737"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-transform-class-properties'],
env: {
test: {
presets: [
@@ -15,7 +15,7 @@ module.exports = {
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.128",
"@ohif/extension-cornerstone": "3.10.0-beta.128",
"@ohif/extension-default": "3.10.0-beta.128",
"@ohif/i18n": "3.10.0-beta.128",
"@ohif/core": "3.12.0-beta.2",
"@ohif/extension-cornerstone": "3.12.0-beta.2",
"@ohif/extension-default": "3.12.0-beta.2",
"@ohif/i18n": "3.12.0-beta.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
@@ -1,10 +1,9 @@
import { utils } from '@ohif/core';
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
import { adaptersPMAP } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import { dicomLoaderService } from '@ohif/extension-cornerstone';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
import { dicomLoaderService, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
function _getDisplaySetsFromSeries(
@@ -57,7 +56,8 @@ function _getDisplaySetsFromSeries(
wadoRoot,
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
supportsWindowLevel: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -227,7 +227,8 @@ async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
return derivedVolume;
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -1,16 +1,10 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid } from '@ohif/ui-next';
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const { displaySets, children, viewportOptions, displaySetOptions, servicesManager } = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
servicesManager.services;
@@ -59,9 +53,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
displaySetOptions.unshift({});
const [pmapDisplaySetOptions] = displaySetOptions;
@@ -93,7 +84,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
});
return (
<Component
<OHIFCornerstoneViewport
{...props}
// Referenced + PMAP displaySets must be passed as parameter in this order
displaySets={[referencedDisplaySet, pmapDisplaySet]}
@@ -104,15 +95,16 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
presentationIds: viewportOptions.presentationIds,
}}
displaySetOptions={[{}, pmapDisplaySetOptions]}
></Component>
/>
);
}, [
extensionManager,
displaySetOptions,
props,
pmapDisplaySet,
viewportOptions.orientation,
viewportOptions.viewportId,
viewportOptions.presentationIds,
uiNotificationService,
]);
// Cleanup the PMAP viewport when the viewport is destroyed
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-transform-class-properties'],
env: {
test: {
presets: [
@@ -15,7 +15,7 @@ module.exports = {
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.128",
"@ohif/extension-cornerstone": "3.10.0-beta.128",
"@ohif/extension-default": "3.10.0-beta.128",
"@ohif/i18n": "3.10.0-beta.128",
"@ohif/core": "3.12.0-beta.2",
"@ohif/extension-cornerstone": "3.12.0-beta.2",
"@ohif/extension-default": "3.12.0-beta.2",
"@ohif/i18n": "3.12.0-beta.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -27,7 +27,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
viewportGridService.setDisplaySetsForViewport({
@@ -1,4 +1,5 @@
import { utils } from '@ohif/core';
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { SOPClassHandlerId } from './id';
import loadRTStruct from './loadRTStruct';
@@ -30,7 +31,7 @@ function _getDisplaySetsFromSeries(
const displaySet = {
Modality: 'RTSTRUCT',
loading: false,
isReconstructable: false, // by default for now since it is a volumetric SEG currently
isReconstructable: false,
displaySetInstanceUID: utils.guid(),
SeriesDescription,
SeriesNumber,
@@ -53,6 +54,7 @@ function _getDisplaySetsFromSeries(
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`,
};
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -85,6 +87,7 @@ function _getDisplaySetsFromSeries(
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
unsubscribe();
}
}
@@ -92,14 +95,22 @@ function _getDisplaySetsFromSeries(
} else {
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
}
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
displaySet.load = ({ headers, createSegmentation = true }) =>
_load(displaySet, servicesManager, extensionManager, headers, createSegmentation);
return [displaySet];
}
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
function _load(
rtDisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager,
headers,
createSegmentation = true
) {
const { SOPInstanceUID } = rtDisplaySet;
const { segmentationService } = servicesManager.services;
if (
@@ -121,16 +132,21 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
rtDisplaySet.structureSet = structureSet;
}
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
})
.catch(error => {
rtDisplaySet.loading = false;
reject(error);
});
if (createSegmentation) {
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
})
.catch(error => {
rtDisplaySet.loading = false;
reject(error);
});
} else {
rtDisplaySet.loading = false;
resolve();
}
});
return loadPromises[SOPInstanceUID];
@@ -171,20 +187,14 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence;
}
function _segmentationExistsInCache(
rtDisplaySet,
segmentationService: AppTypes.SegmentationService
) {
function _segmentationExistsInCache() {
// Todo: fix this
return false;
// This should be abstracted with the CornerstoneCacheService
const rtContourId = rtDisplaySet.displaySetInstanceUID;
const contour = segmentationService.getContour(rtContourId);
return contour !== undefined;
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
return [
{
name: 'dicom-rt',
@@ -1,10 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
import { utils, Types } from '@ohif/extension-cornerstone';
function promptHydrateRT({
servicesManager,
@@ -12,70 +6,20 @@ function promptHydrateRT({
viewportId,
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}
});
}
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
];
const onSubmit = result => {
uiViewportDialogService.hide();
resolve(result);
};
uiViewportDialogService.show({
id: 'promptHydrateRT',
viewportId,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
}: {
servicesManager: AppTypes.ServicesManager;
rtDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateRTDisplaySet: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: rtDisplaySet,
preHydrateCallbacks,
hydrateCallback: hydrateRTDisplaySet,
type: 'RTSTRUCT',
});
}
@@ -1,35 +1,27 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { ViewportActionArrows } from '@ohif/ui';
import { useViewportGrid } from '@ohif/ui-next';
import { utils } from '@ohif/extension-cornerstone';
import {
utils,
usePositionPresentationStore,
OHIFCornerstoneViewport,
} from '@ohif/extension-cornerstone';
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { useSystem } from '@ohif/core/src';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
function OHIFCornerstoneRTViewport(props: withAppTypes) {
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const {
displaySetService,
toolGroupService,
segmentationService,
uiNotificationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -46,31 +38,34 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const rtDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const [{ viewports, activeViewportId }, viewportGridService] = useViewportGrid();
// States
const selectedSegmentObjectIndex: number = 0;
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the RT is opened and segments are loaded into the
// segmentation panel, and RT is also rendered on any viewport that is in the
// same frameOfReferenceUID as the referencedSeriesUID of the RT. However,
// loading basically means RT loading over network and bit unpacking of the
// RT data.
const [isHydrated, setIsHydrated] = useState(rtDisplaySet.isHydrated);
const [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded);
const [element, setElement] = useState(null);
const [processingProgress, setProcessingProgress] = useState({
percentComplete: null,
totalSegments: null,
});
// refs
const referencedDisplaySetRef = useRef(null);
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
// If the referencedDisplaySetInstanceUID is not found, it means the RTStruct series is being
// launched without its corresponding referenced display set (e.g., the RTStruct series is launched using
// series launch /mode?StudyInstanceUIDs=&SeriesInstanceUID).
// In such cases, we attempt to handle this scenario gracefully by
// invoking a custom handler. Ideally, if a user tries to launch a series that isn't viewable,
// (eg.: we can prompt them with an explanation and provide a link to the full study).
if (!referencedDisplaySetInstanceUID) {
const missingReferenceDisplaySetHandler = customizationService.getCustomization(
'missingReferenceDisplaySetHandler'
);
const { handled } = missingReferenceDisplaySetHandler();
if (handled) {
return;
}
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
@@ -80,100 +75,31 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
const onElementDisabled = () => {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: 'stack',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
utils.handleSegmentChange({
direction,
segDisplaySet: rtDisplaySet,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
});
},
[selectedSegmentObjectIndex]
);
useEffect(() => {
if (rtIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateRT({
servicesManager,
viewportId,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateRTDisplaySet,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
hydrateRTDisplaySet: async () => {
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
});
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager, activeViewportId]);
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 => {
@@ -193,14 +119,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
viewPresentation: {},
});
}
if (evt.overlappingSegments) {
uiNotificationService.show({
title: 'Overlapping Segments',
message: 'Overlapping segments detected which is not currently supported',
type: 'warning',
});
}
}
);
@@ -210,7 +128,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}, [rtDisplaySet]);
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
const segmentLoadingSubscription = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
({ percentComplete, numSegments }) => {
setProcessingProgress({
@@ -220,16 +138,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}
);
return () => {
unsubscribe();
};
}, [rtDisplaySet]);
/**
Cleanup the SEG viewport when the viewport is destroyed
*/
useEffect(() => {
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
const displaySetsRemovedSubscription = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
({ displaySetInstanceUIDs }) => {
const activeViewport = viewports.get(activeViewportId);
@@ -243,9 +152,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
);
return () => {
onDisplaySetsRemovedSubscription.unsubscribe();
segmentLoadingSubscription.unsubscribe();
displaySetsRemovedSubscription.unsubscribe();
};
}, []);
}, [rtDisplaySet, displaySetService, viewports, activeViewportId, viewportGridService]);
useEffect(() => {
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
@@ -259,38 +169,33 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(rtDisplaySet.isHydrated);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
// Todo: jump to the center of the first segment
return (
<OHIFCornerstoneViewport
{...props}
displaySets={[referencedDisplaySet, rtDisplaySet]}
viewportOptions={{
viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
}}
/>
);
}, [viewportId, rtDisplaySet, toolGroupId]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
if (
@@ -313,43 +218,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
useEffect(() => {
viewportActionCornersService.addComponents([
{
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/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -1,59 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
switch (isHydrated) {
case true:
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
}
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>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-transform-class-properties'],
env: {
test: {
presets: [
@@ -15,7 +15,7 @@ module.exports = {
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-runtime',
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,10 +33,10 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.128",
"@ohif/extension-cornerstone": "3.10.0-beta.128",
"@ohif/extension-default": "3.10.0-beta.128",
"@ohif/i18n": "3.10.0-beta.128",
"@ohif/core": "3.12.0-beta.2",
"@ohif/extension-cornerstone": "3.12.0-beta.2",
"@ohif/extension-default": "3.12.0-beta.2",
"@ohif/i18n": "3.12.0-beta.2",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
@@ -1,22 +1,13 @@
import dcmjs from 'dcmjs';
import { Types } from '@ohif/core';
import { classes, Types } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core';
import {
segmentation as cornerstoneToolsSegmentation,
Enums as cornerstoneToolsEnums,
utilities,
} from '@cornerstonejs/tools';
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { createReportDialogPrompt } from '@ohif/extension-default';
import { classes, DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore } from '@ohif/core';
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const getTargetViewport = ({ viewportId, viewportGridService }) => {
@@ -46,14 +37,8 @@ const commandsModule = ({
servicesManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
customizationService,
} = servicesManager.services as AppTypes.Services;
const { segmentationService, displaySetService, viewportGridService, toolGroupService } =
servicesManager.services as AppTypes.Services;
const actions = {
/**
@@ -235,7 +220,7 @@ const commandsModule = ({
}
const { label } = segmentation;
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource();
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0];
const {
value: reportName,
@@ -266,6 +251,11 @@ const commandsModule = ({
const { dataset: naturalizedReport } = generatedData;
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out
if (naturalizedReport.StudyID === 'No Study ID') {
naturalizedReport.StudyID = '';
}
await selectedDataSourceConfig.store.dicom(naturalizedReport);
// add the information for where we stored it to the instance as well
@@ -287,21 +277,24 @@ const commandsModule = ({
* converts dataset to downloadable blob.
*
*/
downloadRTSS: ({ segmentationId }) => {
downloadRTSS: async ({ segmentationId }) => {
const segmentations = segmentationService.getSegmentation(segmentationId);
const vtkUtils = {
vtkImageMarchingSquares,
vtkDataArray,
vtkImageData,
};
const RTSS = generateRTSSFromSegmentations(
// inject colors to the segmentIndex
const firstRepresentation =
segmentationService.getRepresentationsForSegmentation(segmentationId)[0];
Object.entries(segmentations.segments).forEach(([segmentIndex, segment]) => {
segment.color = segmentationService.getSegmentColor(
firstRepresentation.viewportId,
segmentationId,
segmentIndex
);
});
const RTSS = await generateRTSSFromSegmentations(
segmentations,
classes.MetadataProvider,
DicomMetadataStore,
cache,
cornerstoneToolsEnums,
vtkUtils
DicomMetadataStore
);
try {
@@ -314,56 +307,9 @@ 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',
'ThresholdCircularBrushDynamic',
'ThresholdSphereBrushDynamic',
],
}) => {
const toolGroupIds = toolGroupService.getToolGroupIds();
if (!toolGroupIds?.length) {
return;
}
for (const toolGroupId of toolGroupIds) {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
toolNames?.forEach(toolName => {
toolGroup.setToolConfiguration(toolName, {
threshold: {
range: value,
},
});
});
}
},
};
const definitions = {
/**
* Obsolete?
*/
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
/**
* Obsolete?
*/
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
@@ -380,12 +326,6 @@ const commandsModule = ({
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
setBrushSize: {
commandFn: actions.setBrushSize,
},
setThresholdRange: {
commandFn: actions.setThresholdRange,
},
};
return {
@@ -1,12 +1,13 @@
import { utils } from '@ohif/core';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { metaData, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
import { dicomlabToRGB } from './utils/dicomlabToRGB';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7'];
const loadPromises = {};
@@ -33,7 +34,7 @@ function _getDisplaySetsFromSeries(
const displaySet = {
Modality: 'SEG',
loading: false,
isReconstructable: true, // by default for now since it is a volumetric SEG currently
isReconstructable: false,
displaySetInstanceUID: utils.guid(),
SeriesDescription,
SeriesNumber,
@@ -57,6 +58,7 @@ function _getDisplaySetsFromSeries(
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SEG')}`,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -89,12 +91,14 @@ function _getDisplaySetsFromSeries(
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
unsubscribe();
}
}
);
} else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
}
displaySet.load = async ({ headers }) =>
@@ -177,12 +181,16 @@ async function _loadSegments({
throw new Error('referencedDisplaySet is missing for SEG');
}
const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
let { imageIds } = referencedDisplaySet;
if (!imageIds) {
// try images
const { images } = referencedDisplaySet;
imageIds = images.map(image => image.imageId);
}
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
@@ -190,11 +198,10 @@ async function _loadSegments({
});
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
const results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
{ metadataProvider: metaData, tolerance }
);
let usedRecommendedDisplayCIELabValue = true;
@@ -211,15 +218,6 @@ async function _loadSegments({
}
});
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({
@@ -238,7 +236,8 @@ function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -45,7 +45,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
if (!toolNames) {
return {
disabled: false,
// isActive: true,
// isActive: false,
};
}
@@ -1,7 +1,14 @@
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
function createSEGToolGroupAndAddTools({
commandsManager,
toolGroupService,
customizationService,
toolGroupId,
}) {
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
const updatedTools = commandsManager.run('initializeSegmentLabelTool', { tools });
return toolGroupService.createToolGroupAndAddTools(toolGroupId, updatedTools);
}
export default createSEGToolGroupAndAddTools;
@@ -1,10 +1,4 @@
import { ButtonEnums } from '@ohif/ui';
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
HYDRATE_SEG: 5,
};
import { utils, Types } from '@ohif/extension-cornerstone';
function promptHydrateSEG({
servicesManager,
@@ -12,71 +6,20 @@ function promptHydrateSEG({
viewportId,
preHydrateCallbacks,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
resolve(isHydrated);
}, 0);
}
});
}
function _askHydrate(uiViewportDialogService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const actions = [
{
id: 'no-hydrate',
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
id: 'yes-hydrate',
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.HYDRATE_SEG,
},
];
const onSubmit = result => {
uiViewportDialogService.hide();
resolve(result);
};
uiViewportDialogService.show({
viewportId,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
onSubmit(RESPONSE.HYDRATE_SEG);
}
},
});
}: {
servicesManager: AppTypes.ServicesManager;
segDisplaySet: AppTypes.DisplaySet;
viewportId: string;
preHydrateCallbacks?: Types.HydrationCallback[];
hydrateCallback: Types.HydrationCallback;
}) {
return utils.promptHydrationDialog({
servicesManager,
viewportId,
displaySet: segDisplaySet as AppTypes.DisplaySet,
preHydrateCallbacks,
hydrateCallback,
type: 'SEG',
});
}
@@ -1,36 +1,24 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionArrows } from '@ohif/ui';
import { useViewportGrid } from '@ohif/ui-next';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { utils } from '@ohif/extension-cornerstone';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const {
children,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
const { servicesManager, commandsManager } = useSystem();
const { children, displaySets, viewportOptions } = props as {
children: React.ReactNode;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const viewportId = viewportOptions.viewportId;
const {
displaySetService,
toolGroupService,
segmentationService,
customizationService,
viewportActionCornersService,
} = servicesManager.services;
const { displaySetService, toolGroupService, segmentationService, customizationService } =
servicesManager.services;
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
'ui.loadingIndicatorTotalPercent'
@@ -47,7 +35,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
let selectedSegmentObjectIndex: number = 0;
const { setPositionPresentation } = usePositionPresentationStore();
// Hydration means that the SEG is opened and segments are loaded into the
@@ -55,9 +42,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// same frameOfReferenceUID as the referencedSeriesUID of the SEG. However,
// loading basically means SEG loading over network and bit unpacking of the
// SEG data.
const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated);
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
const [element, setElement] = useState(null);
const [processingProgress, setProcessingProgress] = useState({
percentComplete: null,
totalSegments: null,
@@ -69,6 +54,21 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
// If the referencedDisplaySetInstanceUID is not found, it means the SEG series is being
// launched without its corresponding referenced display set (e.g., the SEG series is launched using
// series launch /mode?StudyInstanceUIDs=&SeriesInstanceUID).
// In such cases, we attempt to handle this scenario gracefully by
// invoking a custom handler. Ideally, if a user tries to launch a series that isn't viewable,
// (eg.: we can prompt them with an explanation and provide a link to the full study).
if (!referencedDisplaySetInstanceUID) {
const missingReferenceDisplaySetHandler = customizationService.getCustomization(
'missingReferenceDisplaySetHandler'
);
const { handled } = missingReferenceDisplaySetHandler();
if (handled) {
return;
}
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
@@ -82,36 +82,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
displaySet: referencedDisplaySet,
metadata: referencedDisplaySetMetadata,
};
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
const onElementDisabled = () => {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportId }) => {
commandsManager.runCommand('storePresentation', {
viewportId,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
// Todo: jump to the center of the first segment
return (
<Component
<OHIFCornerstoneViewport
{...props}
displaySets={[segDisplaySet]}
viewportOptions={{
@@ -123,66 +97,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
onElementEnabled(evt);
}}
onElementDisabled={onElementDisabled}
></Component>
/>
);
}, [viewportId, segDisplaySet, toolGroupId]);
const onSegmentChange = useCallback(
direction => {
utils.handleSegmentChange({
direction,
segDisplaySet: segDisplaySet,
viewportId,
selectedSegmentObjectIndex,
segmentationService,
});
},
[selectedSegmentObjectIndex]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
useEffect(() => {
if (segIsLoading) {
return;
}
// if not active viewport, return
if (viewportId !== activeViewportId) {
return;
}
promptHydrateSEG({
servicesManager,
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
hydrateCallback: async () => {
await commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: segDisplaySet,
viewportId,
});
return true;
},
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, commandsManager, activeViewportId]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
@@ -271,7 +214,12 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
toolGroup = createSEGToolGroupAndAddTools({
commandsManager,
toolGroupService,
customizationService,
toolGroupId,
});
return () => {
// remove the segmentation representations if seg displayset changed
@@ -283,54 +231,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
};
}, []);
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();
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
useEffect(() => {
viewportActionCornersService.addComponents([
{
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/pane:visible'
}
></ViewportActionArrows>
),
indexPriority: 0,
location: viewportActionCornersService.LOCATIONS.topRight,
},
]);
}, [
activeViewportId,
isHydrated,
onSegmentChange,
onStatusClick,
viewportActionCornersService,
viewportId,
]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -1,59 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
let ToolTipMessage = null;
let StatusIcon = null;
switch (isHydrated) {
case true:
StatusIcon = () => <Icons.ByName name="status-alert" />;
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
break;
case false:
StatusIcon = () => (
<Icons.ByName
className="text-aqua-pale"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
}
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>
{!isHydrated && (
<ViewportActionButton onInteraction={onStatusClick}>{loadStr}</ViewportActionButton>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
File diff suppressed because it is too large. Load diff
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -34,11 +34,11 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.128",
"@ohif/extension-cornerstone": "3.10.0-beta.128",
"@ohif/extension-measurement-tracking": "3.10.0-beta.128",
"@ohif/ui": "3.10.0-beta.128",
"dcmjs": "*",
"@ohif/core": "3.12.0-beta.2",
"@ohif/extension-cornerstone": "3.12.0-beta.2",
"@ohif/extension-measurement-tracking": "3.12.0-beta.2",
"@ohif/ui": "3.12.0-beta.2",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}
@@ -10,6 +10,15 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF;
interface Options {
SeriesDescription?: string;
SeriesInstanceUID?: string;
SeriesNumber?: number;
InstanceNumber?: number;
SeriesDate?: string;
SeriesTime?: string;
}
/**
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
@@ -17,18 +26,13 @@ const { log } = OHIF;
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
*
*/
const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
const _generateReport = (measurementData, additionalFindingTypes, options: Options = {}) => {
const filteredToolState = getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
);
const report = MeasurementReport.generateReport(
filteredToolState,
metaData,
utilities.worldToImageCoords,
options
);
const report = MeasurementReport.generateReport(filteredToolState, metaData, options);
const { dataset } = report;
@@ -37,12 +41,15 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
if (typeof dataset.SpecificCharacterSet === 'undefined') {
dataset.SpecificCharacterSet = 'ISO_IR 192';
}
dataset.InstanceNumber = options.InstanceNumber ?? 1;
return dataset;
};
const commandsModule = (props: withAppTypes) => {
const { servicesManager, extensionManager } = props;
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
const { servicesManager, extensionManager, commandsManager } = props;
const { customizationService } = servicesManager.services;
const actions = {
changeColorMeasurement: ({ uid }) => {
@@ -117,6 +124,9 @@ const commandsModule = (props: withAppTypes) => {
console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content');
}
if (!naturalizedReport.SOPClassUID) {
throw new Error('No sop class uid');
}
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
@@ -148,34 +158,18 @@ const commandsModule = (props: withAppTypes) => {
* Loads measurements by hydrating and loading the SR for the given display set instance UID
* and displays it in the active viewport.
*/
loadSRMeasurements: ({ displaySetInstanceUID }) => {
const { SeriesInstanceUIDs } = hydrateStructuredReport(
hydrateStructuredReport: ({ displaySetInstanceUID }) => {
return hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
{
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
},
]);
}
},
};
const definitions = {
downloadReport: {
commandFn: actions.downloadReport,
},
storeMeasurements: {
commandFn: actions.storeMeasurements,
},
loadSRMeasurements: {
commandFn: actions.loadSRMeasurements,
},
downloadReport: actions.downloadReport,
storeMeasurements: actions.storeMeasurements,
hydrateStructuredReport: actions.hydrateStructuredReport,
};
return {
@@ -1,25 +1,24 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ExtensionManager, useToolbar } from '@ohif/core';
import React, { useCallback, useEffect, useState } from 'react';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import { ViewportActionArrows } from '@ohif/ui';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
import { usePositionPresentationStore } from '@ohif/extension-cornerstone';
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
import { useViewportGrid } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
function OHIFCornerstoneSRMeasurementViewport(props) {
const { servicesManager } = useSystem();
const { children, dataSource, displaySets, viewportOptions } = props as {
children: React.ReactNode;
dataSource: unknown;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportOptions;
};
const { displaySetService, viewportActionCornersService } = servicesManager.services;
const { displaySetService } = servicesManager.services;
const viewportId = viewportOptions.viewportId;
@@ -34,40 +33,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const [viewportGrid, viewportGridService] = useViewportGrid();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
const [element, setElement] = useState(null);
const { viewports, activeViewportId } = viewportGrid;
const { t } = useTranslation('Common');
// Optional hook into tracking extension, if present.
let trackedMeasurements;
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
MEASUREMENT_TRACKING_EXTENSION_ID
);
if (hasMeasurementTrackingExtension) {
const contextModule = extensionManager.getModuleEntry(
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
);
const tracked = useContext(contextModule.context);
trackedMeasurements = tracked?.[0];
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
* when multiple viewports are used.
*/
const setTrackingIdentifiers = useCallback(
measurementSelected => {
const { measurements } = srDisplaySet;
@@ -93,22 +63,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const updateViewport = useCallback(
newMeasurementSelected => {
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
const { StudyInstanceUID, displaySetInstanceUID } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error?
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@@ -140,10 +100,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
return null;
}
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { measurements } = srDisplaySet;
const measurement = measurements[measurementSelected];
@@ -152,7 +108,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
}
return (
<Component
<OHIFCornerstoneViewport
{...props}
// should be passed second since we don't want SR displaySet to
// override the activeImageDisplaySetData
@@ -176,27 +132,10 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
onElementEnabled(evt);
}}
isJumpToMeasurementDisabled={true}
></Component>
/>
);
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
const onMeasurementChange = useCallback(
direction => {
let newMeasurementSelected = measurementSelected;
newMeasurementSelected += direction;
if (newMeasurementSelected >= measurementCount) {
newMeasurementSelected = 0;
} else if (newMeasurementSelected < 0) {
newMeasurementSelected = measurementCount - 1;
}
setTrackingIdentifiers(newMeasurementSelected);
updateViewport(newMeasurementSelected);
},
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
);
/**
Cleanup the SR viewport when the viewport is destroyed
*/
@@ -231,8 +170,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
@@ -255,42 +192,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
component: _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable: srDisplaySet.isRehydratable,
isLocked,
t,
servicesManager,
}),
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, srDisplaySet, t, viewportActionCornersService, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -327,8 +228,6 @@ OHIFCornerstoneSRMeasurementViewport.propTypes = {
children: PropTypes.node,
viewportLabel: PropTypes.string,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.object.isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
async function _getViewportReferencedDisplaySetData(
@@ -353,6 +252,9 @@ async function _getViewportReferencedDisplaySetData(
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet?.images) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
@@ -372,130 +274,4 @@ async function _getViewportReferencedDisplaySetData(
return { referencedDisplaySetMetadata, referencedDisplaySet };
}
function _getStatusComponent({
srDisplaySet,
viewportId,
isRehydratable,
isLocked,
t,
servicesManager,
}) {
const loadStr = t('LOAD');
// 1 - Incompatible
// 2 - Locked
// 3 - Rehydratable / Open
const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
let ToolTipMessage = null;
let StatusIcon = null;
switch (state) {
case 1:
StatusIcon = () => (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is not compatible
<br />
with this application.
</div>
);
break;
case 2:
StatusIcon = () => (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
ToolTipMessage = () => (
<div>
This structured report is currently read-only
<br />
because you are tracking measurements in
<br />
another viewport.
</div>
);
break;
case 3:
StatusIcon = () => (
<Icons.ByName
className="text-aqua-pale h-4 w-4"
name="status-untracked"
/>
);
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
}
const StatusArea = () => {
const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({
servicesManager,
buttonSection: 'loadSRMeasurements',
});
const commandOptions = {
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
viewportId,
};
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 className="h-4 w-4" />
<span className="ml-1">SR</span>
</div>
{state === 3 && (
<>
{loadSRMeasurementsButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
const tool = (
<Component
key={id}
id={id}
onInteraction={args => onInteraction({ ...args, ...commandOptions })}
{...componentProps}
/>
);
return <div key={id}>{tool}</div>;
})}
</>
)}
</div>
);
};
return (
<>
{ToolTipMessage && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
>
<ToolTipMessage />
</TooltipContent>
</Tooltip>
)}
{!ToolTipMessage && <StatusArea />}
</>
);
}
export default OHIFCornerstoneSRMeasurementViewport;
@@ -1,4 +1,5 @@
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import { adaptersSR } from '@cornerstonejs/adapters';
@@ -14,8 +15,12 @@ import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { MetadataProvider: metadataProvider } = classes;
const {
TEXT_ANNOTATION_POSITION,
COMMENT_CODE,
CodeScheme: Cornerstone3DCodeScheme,
} = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata;
@@ -90,6 +95,7 @@ function _getDisplaySetsFromSeries(
SeriesDescription,
SeriesNumber,
SeriesDate,
SeriesTime,
ConceptNameCodeSequence,
SOPClassUID,
} = instance;
@@ -106,6 +112,7 @@ function _getDisplaySetsFromSeries(
SeriesDescription,
SeriesNumber,
SeriesDate,
SeriesTime,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
@@ -120,6 +127,7 @@ function _getDisplaySetsFromSeries(
sopClassUids,
instance,
addInstances,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
};
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
@@ -212,6 +220,12 @@ async function _load(
});
}
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
return (
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
);
}
/**
* Checks if measurements can be added to a display set.
*
@@ -232,18 +246,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
measurement => measurement.loaded === false
);
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
if (!unloadedMeasurements.length || newDisplaySet.unsupported) {
return;
}
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
@@ -262,6 +268,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D';
const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement'
@@ -276,9 +283,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
if (
is3DSR &&
is3DMeasurement &&
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
) {
addSRAnnotation(measurement, null, null);
measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
continue;
}
@@ -352,7 +365,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
*/
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -464,11 +478,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
return _processTID1410Measurement(mergedContentSequence);
}
@@ -506,12 +516,25 @@ function _processTID1410Measurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const { ConceptNameCodeSequence: conceptNameItem } = graphicItem;
const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem;
const graphicCode = `${graphicDesignator}:${graphicValue}`;
const pointDataItem = _getCoordsFromSCOORDOrSCOORD3D(graphicItem);
const is3DMeasurement = pointDataItem.ValueType === 'SCOORD3D';
const pointLength = is3DMeasurement ? 3 : 2;
const pointsLength = pointDataItem.GraphicData.length / pointLength;
const measurement = {
loaded: false,
labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
coords: [pointDataItem],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
graphicCode,
is3DMeasurement,
pointsLength,
graphicType: pointDataItem.GraphicType,
};
NUMContentItems.forEach(item => {
@@ -562,6 +585,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
);
const commentSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator &&
item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value
);
const measurement = {
loaded: false,
labels: [],
@@ -570,6 +599,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
};
if (commentSites) {
for (const group of commentSites) {
if (group.TextValue) {
measurement.labels.push({ label: group.TextValue, value: '' });
}
}
}
if (
finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
@@ -721,4 +758,17 @@ function _getSequenceAsArray(sequence) {
return Array.isArray(sequence) ? sequence : [sequence];
}
function isScoordOr3d(group) {
return group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D';
}
function isTextPosition(group) {
const concept = group.ConceptNameCodeSequence[0];
return (
concept &&
concept.CodeValue === TEXT_ANNOTATION_POSITION.value &&
concept.CodingSchemeDesignator === TEXT_ANNOTATION_POSITION.schemeDesignator
);
}
export default getSopClassHandlerModule;
+1 -26
View File
@@ -12,7 +12,6 @@ import {
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@@ -57,7 +56,7 @@ export default function init({
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
);
// Modify annotation tools to use dashed lines on SR
@@ -77,28 +76,4 @@ export default function init({
SRRectangleROI: dashedLine,
global: {},
});
measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
({ viewportId, measurement, isConsumed }) => {
if (isConsumed) {
return;
}
try {
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { viewPlaneNormal } = currentViewport.getCamera();
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
currentViewport as StackViewport,
measurement.points[0],
viewPlaneNormal
);
const imageIndex = (currentViewport as StackViewport)
.getImageIds()
.indexOf(referencedImageId);
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error);
}
}
);
}
@@ -1,11 +1,7 @@
import React from 'react';
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
import { ViewportActionButton } from '@ohif/ui';
import i18n from '@ohif/i18n';
export default function onModeEnter({ servicesManager }) {
const { displaySetService, toolbarService } = servicesManager.services;
const { displaySetService } = servicesManager.services;
const displaySetCache = displaySetService.getDisplaySetCache();
const srDisplaySets = [...displaySetCache.values()].filter(
@@ -16,24 +12,4 @@ export default function onModeEnter({ servicesManager }) {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
toolbarService.addButtons([
{
// A base/default button for loading measurements. It is added to the toolbar below.
// Customizations to this button can be made in the mode or by another extension.
// For example, the button label can be changed and/or the command to clear
// the measurements can be dropped.
id: 'loadSRMeasurements',
component: props => (
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
),
props: {
commands: ['clearMeasurements', 'loadSRMeasurements'],
},
},
]);
// The toolbar used in the viewport's status bar. Modes and extensions can further customize
// it to optionally add other buttons.
toolbarService.createButtonSection('loadSRMeasurements', ['loadSRMeasurements']);
}
@@ -1,4 +1,4 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import { type Types } from '@cornerstonejs/core';
import {
annotation,
drawing,
@@ -7,13 +7,8 @@ const SRSCOOR3DProbe = {
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
toMeasurement: ({ servicesManager, getValueTypeFromToolType }, csToolsEventDetail) => {
const { displaySetService } = servicesManager.services;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
@@ -22,14 +17,25 @@ const SRSCOOR3DProbe = {
return null;
}
const { toolName } = metadata;
const { toolName, FrameOfReferenceUID } = metadata;
const { points } = data.handles;
const displaySets = displaySetService
.getActiveDisplaySets()
.filter(ds => ds.FrameOfReferenceUID === FrameOfReferenceUID);
const displaySet = displaySets.filter(ds => ds.isReconstructable)[0] || displaySets[0];
const { StudyInstanceUID: referenceStudyUID, SeriesInstanceUID: referenceSeriesUID } =
displaySets[0] || {};
const displayText = getDisplayText(annotation);
return {
uid: annotationUID,
points,
metadata,
referenceStudyUID,
referenceSeriesUID,
displaySetInstanceUID: displaySet?.displaySetInstanceUID,
toolName: metadata.toolName,
label: data.label,
displayText: displayText,
@@ -56,7 +62,10 @@ function getDisplayText(annotation) {
}
}
return displayText;
return {
primary: displayText,
secondary: [],
};
}
export default SRSCOOR3DProbe;
@@ -1,9 +1,12 @@
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core';
import { adaptersSR } from '@cornerstonejs/adapters';
import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => {
@@ -25,7 +28,12 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
}
if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint;
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(
measurement.TrackingIdentifier
);
if (!adapter) {
toolName = toolNames.SRSCOORD3DPoint;
}
// get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
@@ -63,5 +71,4 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
* was not triggering annotation_added events.
*/
annotation.state.addAnnotation(SRAnnotation);
console.debug('Adding SR annotation:', SRAnnotation);
}
@@ -1,17 +1,11 @@
import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF;
import { NO_IMAGE_ID } from '@cornerstonejs/adapters';
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) {
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
return;
}
const imageId = annotation.metadata.referencedImageId;
const imageId = annotation.metadata?.referencedImageId ?? NO_IMAGE_ID;
if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {};
@@ -11,7 +11,11 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
* @returns {string} The extracted label.
*/
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
const { findingSites = [], finding, annotation } = toolData;
if (annotation.data.label) {
return annotation.data.label;
}
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
@@ -36,6 +36,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
ValueType,
imageId,
});
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
@@ -90,6 +98,13 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
imageId,
});
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
@@ -1,15 +1,16 @@
import { utilities, metaData } from '@cornerstonejs/core';
import { utilities, metaData, type Types } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import { vec3 } from 'gl-matrix';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
import { annotation as CsAnnotation, type Types as ToolTypes } from '@cornerstonejs/tools';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
@@ -37,8 +38,10 @@ const convertSites = (codingValues, sites) => {
};
/**
* Hydrates a structured report, for default viewports.
*
* Hydrates a structured report
* Handles 2d and 3d hydration from SCOORD and SCOORD3D points
* For 3D hydration, chooses a volume display set to display with
* FOr 2D hydration, chooses the (first) display set containing the referenced image.
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
@@ -88,7 +91,7 @@ export default function hydrateStructuredReport(
// Mapping of legacy datasets is now directly handled by adapters module
const datasetToUse = instance;
// Use dcmjs to generate toolState.
// Use CS3D adapters to generate toolState.
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
@@ -96,7 +99,6 @@ export default function hydrateStructuredReport(
// that measurements were added to the display set are the same order as
// the measurementGroups in the instance.
sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData
);
@@ -147,6 +149,9 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (!imageId) {
continue;
}
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
@@ -160,38 +165,55 @@ export default function hydrateStructuredReport(
}
}
/**
* Gets reference data for what frame of reference and the referenced
* image id, or for 3d measurements, the volumeId to apply this annotation to.
*/
function getReferenceData(toolData): ToolTypes.AnnotationMetadata {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
if (!imageId) {
return getReferenceData3D(toolData, servicesManager);
}
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
return {
referencedImageId: imageId,
FrameOfReferenceUID,
};
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
toolData.uid = guid();
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
const referenceData = getReferenceData(toolData);
const { imageId } = referenceData;
const annotation = {
annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data,
metadata: {
...referenceData,
toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
},
};
utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata);
const source = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@@ -225,7 +247,7 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
@@ -238,3 +260,92 @@ export default function hydrateStructuredReport(
SeriesInstanceUIDs,
};
}
/**
* For 3d annotations, there are often several display sets which could
* be used to display the annotation. Choose the first annotation with the
* same frame of reference that is reconstructable, or the first display set
* otherwise.
*/
function chooseDisplaySet(displaySets, annotation) {
if (!displaySets?.length) {
console.warn('No display set found for', annotation);
return;
}
if (displaySets.length === 1) {
return displaySets[0];
}
const volumeDs = displaySets.find(ds => ds.isReconstructable);
if (volumeDs) {
return volumeDs;
}
return displaySets[0];
}
/**
* Gets the additional reference data appropriate for a 3d reference.
* This will choose a volume id, frame of reference and a plane restriction.
*/
function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
const { FrameOfReferenceUID } = toolData.annotation.metadata;
const { points } = toolData.annotation.data.handles;
const { displaySetService } = servicesManager.services;
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID
);
if (!displaySetsFOR.length || !points?.length) {
return {
FrameOfReferenceUID,
};
}
const ds = chooseDisplaySet(displaySetsFOR, toolData.annotation);
const cameraView = chooseCameraView(ds, points);
const viewReference = {
...cameraView,
volumeId: ds.displaySetInstanceUID,
FrameOfReferenceUID,
};
utilities.updatePlaneRestriction(points, viewReference);
return viewReference;
}
/**
* Chooses a possible camera view - right now this is fairly basic,
* just setting the unknowns to null.
*/
function chooseCameraView(_ds, points) {
const selectedPoints = choosePoints(points);
const cameraFocalPoint = <Point3>centerOf(selectedPoints);
// These are sufficient to be null for now and can be set on first view
let viewPlaneNormal: Types.Point3 = null;
let viewUp: Types.Point3 = null;
return {
cameraFocalPoint,
viewPlaneNormal,
viewUp,
};
}
function centerOf(points) {
const scale = 1 / points.length;
const center = vec3.create();
for (const point of points) {
vec3.scaleAndAdd(center, center, point, scale);
}
return center;
}
function choosePoints(points) {
if (points.length === 1 || points.length === 2) {
return points;
}
const firstIndex = 0;
const secondIndex = Math.ceil(points.length / 4);
const thirdIndex = Math.ceil(points.length / 2);
// TODO - check if colinear, if so try to find another 3 points.
const newPoints = [points[firstIndex], points[secondIndex], points[thirdIndex]];
return newPoints;
}
@@ -22,13 +22,20 @@ export default function isRehydratable(displaySet, mappings) {
const { measurements } = displaySet;
for (let i = 0; i < measurements.length; i++) {
const { TrackingIdentifier } = measurements[i] || {};
if (!TrackingIdentifier) {
console.warn('No tracking identifier for measurement ', measurements[i]);
const measurement = measurements[i];
if (!measurement) {
continue;
}
const { TrackingIdentifier = '', graphicType, graphicCode, pointsLength } = measurement;
if (!TrackingIdentifier && !graphicType) {
console.warn('No tracking identifier or graphicType for measurement ', measurement);
continue;
}
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(TrackingIdentifier);
const hydratable = adapter && mappingDefinitions.has(adapter.toolType);
const adapters = MeasurementReport.getAdaptersForTypes(graphicCode, graphicType, pointsLength);
const hydratable =
(adapter && mappingDefinitions.has(adapter.toolType)) ||
(adapters && adapters.some(adapter => mappingDefinitions.has(adapter.toolType)));
if (hydratable) {
return true;
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,12 +29,12 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.128",
"@ohif/extension-cornerstone": "3.10.0-beta.128",
"@ohif/extension-default": "3.10.0-beta.128",
"@ohif/i18n": "3.10.0-beta.128",
"@ohif/ui": "3.10.0-beta.128",
"dcmjs": "*",
"@ohif/core": "3.12.0-beta.2",
"@ohif/extension-cornerstone": "3.12.0-beta.2",
"@ohif/extension-default": "3.12.0-beta.2",
"@ohif/i18n": "3.12.0-beta.2",
"@ohif/ui": "3.12.0-beta.2",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}
@@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
}
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
debugger;
const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
@@ -263,7 +263,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
}
}
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate });
},
swapComputedWithDynamicDisplaySet: () => {
// Todo: this assumes there is only one dynamic display set in the viewer
@@ -313,7 +313,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
}
}
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate });
},
createNewLabelMapForDynamicVolume: async ({ label }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
@@ -5,6 +5,8 @@ import { PanelSegmentation } from '@ohif/extension-cornerstone';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
const { toolbarService } = servicesManager.services;
const wrappedDynamicDataPanel = () => {
return (
<DynamicDataPanel
@@ -19,7 +21,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
return (
<>
<Toolbox
buttonSectionId="dynamic-toolbox"
buttonSectionId={toolbarService.sections.dynamicToolbox}
title="Threshold Tools"
/>
<PanelSegmentation
@@ -1,12 +1,24 @@
import React, { useState } from 'react';
import { Button, PanelSection, ButtonGroup, IconButton, InputNumber } from '@ohif/ui';
import { Icons, Tooltip, TooltipTrigger, TooltipContent, Numeric } from '@ohif/ui-next';
import {
Tabs,
TabsList,
TabsTrigger,
Button,
PanelSection,
Icons,
Tooltip,
TooltipTrigger,
TooltipContent,
Numeric,
} from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
sizeClassName: 'w-[58px] h-[28px]',
arrowsDirection: 'horizontal',
labelPosition: 'bottom',
// Helper function to safely convert any value to uppercase string
const toUpperCaseString = value => {
if (value === null || value === undefined) {
return '';
}
return String(value).toUpperCase();
};
const Header = ({ title, tooltip }) => (
@@ -16,18 +28,18 @@ const Header = ({ title, tooltip }) => (
<span>
<Icons.ByName
name="info-link"
className="text-primary-active h-[14px] w-[14px]"
className="text-primary h-3 w-3"
/>
</span>
</TooltipTrigger>
<TooltipContent
sideOffset={4}
className="bg-primary-dark max-w-xs p-2 text-white"
className="max-w-xs"
>
<div>{tooltip}</div>
</TooltipContent>
</Tooltip>
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
<span className="text-muted-foreground text-xs uppercase tracking-wide">{title}</span>
</div>
);
@@ -51,112 +63,138 @@ const DynamicVolumeControls = ({
const [computedView, setComputedView] = useState(false);
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
// Wrapper for onGenerate to handle potential errors
const handleGenerate = () => {
try {
if (typeof onGenerate === 'function') {
onGenerate(computeViewMode);
} else {
console.error('onGenerate is not a function', onGenerate);
}
} catch (error) {
console.error('Error in onGenerate:', error);
}
};
return (
<div className="flex select-none flex-col">
<PanelSection
title="Controls"
childrenClassName="space-y-4 pb-5 px-5"
>
<div className="mt-2">
<Header
title="View"
tooltip={
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
}
/>
<ButtonGroup className="mt-2 w-full">
<button
className="w-1/2"
onClick={() => {
setComputedView(false);
onDynamicClick?.();
<PanelSection defaultOpen={true}>
<PanelSection.Header>Controls</PanelSection.Header>
<PanelSection.Content className="bg-muted space-y-4 px-5 pt-2 pb-4">
<div className="mb-4">
<Header
title="View"
tooltip={
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
}
/>
<Tabs
value={computedView ? 'computed' : '4d'}
onValueChange={value => {
const isComputed = value === 'computed';
setComputedView(isComputed);
if (!isComputed && typeof onDynamicClick === 'function') {
onDynamicClick();
}
}}
className="my-2 w-full"
>
4D
</button>
<button
className="w-1/2"
onClick={() => {
setComputedView(true);
}}
>
Computed
</button>
</ButtonGroup>
</div>
<div>
<DimensionGroupControls
onPlayPauseChange={onPlayPauseChange}
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<ButtonGroup
className="mt-2 w-full"
separated={true}
>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
>
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
>
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
</button>
<button
className="w-1/2"
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
>
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups}
values={rangeValues}
onChange={onDoubleRangeChange}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
<TabsList className="w-full">
<TabsTrigger
value="4d"
className="w-1/2"
>
4D
</TabsTrigger>
<TabsTrigger
value="computed"
className="w-1/2"
>
Computed
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Button
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
onClick={() => {
onGenerate(computeViewMode);
}}
>
Generate
</Button>
</div>
<div>
<DimensionGroupControls
onPlayPauseChange={onPlayPauseChange}
isPlaying={isPlaying}
computedView={computedView}
// fps
fps={fps}
onFpsChange={onFpsChange}
minFps={minFps}
maxFps={maxFps}
//
numDimensionGroups={numDimensionGroups}
onDimensionGroupChange={onDimensionGroupChange}
currentDimensionGroupNumber={currentDimensionGroupNumber}
/>
</div>
<div className={`mt-3 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
<Header
title="Computed Operation"
tooltip={
<div>
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to
be applied to the data set.
<br /> Range Slider: Choose the numeric range of dimension groups within which the
operation will be performed.
<br />
Generate Button: Execute the chosen operation on the specified range of data.
</div>
}
/>
<Tabs
value={String(computeViewMode)}
onValueChange={value => {
setComputeViewMode(value);
}}
className="mt-2 w-full"
>
<TabsList className="w-full gap-1">
{' '}
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUM)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUM)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.AVERAGE)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.AVERAGE)}
</TabsTrigger>
<TabsTrigger
value={String(Enums.DynamicOperatorType.SUBTRACT)}
className="w-1/3"
>
{toUpperCaseString(Enums.DynamicOperatorType.SUBTRACT)}
</TabsTrigger>
</TabsList>
</Tabs>
<div className="mt-2 w-full">
<Numeric.Container
mode="doubleRange"
min={1}
max={numDimensionGroups || 1}
values={rangeValues || [1, numDimensionGroups || 1]}
onChange={onDoubleRangeChange || (() => {})}
>
<Numeric.DoubleRange showNumberInputs />
</Numeric.Container>
</div>
<Button
variant="default"
size="sm"
className="mt-2 h-[26px] w-[115px] self-start p-0"
onClick={handleGenerate}
>
Generate
</Button>
</div>
</PanelSection.Content>
</PanelSection>
</div>
);
@@ -179,7 +217,7 @@ function DimensionGroupControls({
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
return (
<div className={computedView && 'ohif-disabled'}>
<div className={computedView ? 'ohif-disabled' : ''}>
<Header
title="4D Controls"
tooltip={
@@ -192,32 +230,56 @@ function DimensionGroupControls({
}
/>
<div className="mt-3 flex justify-between">
<IconButton
<Button
id="play-pause-button"
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
onClick={() => onPlayPauseChange(!isPlaying)}
variant="secondary"
size="default"
className="w-[58px]"
onClick={() => {
if (typeof onPlayPauseChange === 'function') {
onPlayPauseChange(!isPlaying);
}
}}
>
<Icons.ByName
name={getPlayPauseIconName()}
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
className="text-foreground h-[24px] w-[24px]"
/>
</IconButton>
<InputNumber
value={currentDimensionGroupNumber}
onChange={onDimensionGroupChange}
minValue={1}
maxValue={numDimensionGroups}
label="Group"
{...controlClassNames}
/>
<InputNumber
value={fps}
onChange={onFpsChange}
minValue={minFps}
maxValue={maxFps}
{...controlClassNames}
label="FPS"
/>
</Button>
<Numeric.Container
mode="stepper"
value={currentDimensionGroupNumber || 1}
onChange={onDimensionGroupChange || (() => {})}
min={1}
max={numDimensionGroups || 1}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="h-[28px] w-[58px]"
direction="horizontal"
/>
<Numeric.Label className="text-muted-foreground mt-1 text-sm">Frame</Numeric.Label>
</div>
</Numeric.Container>
<Numeric.Container
mode="stepper"
value={fps || 1}
onChange={onFpsChange || (() => {})}
min={minFps || 1}
max={maxFps || 30}
step={1}
>
<div className="flex flex-col items-center">
<Numeric.NumberStepper
className="h-[28px] w-[58px]"
direction="horizontal"
/>
<Numeric.Label className="text-muted-foreground mt-1 text-sm">FPS</Numeric.Label>
</div>
</Numeric.Container>
</div>
</div>
);
@@ -1,71 +0,0 @@
import React from 'react';
import { InputDoubleRange } from '@ohif/ui';
import { Select } from '@ohif/ui';
import { Button } from '@ohif/ui';
import PropTypes from 'prop-types';
const GenerateVolume = ({
rangeValues,
handleSliderChange,
operationsUI,
options,
handleGenerateOptionsChange,
onGenerateImage,
returnTo4D,
displayingComputedVolume,
}) => {
return (
<>
<div>
<div className="mb-2 text-white">Computed Image</div>
<Select
closeMenuOnSelect={true}
className="border-primary-main mr-2 bg-black text-white "
options={operationsUI}
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
value={options.Operation}
onChange={({ value }) => {
handleGenerateOptionsChange({
Operation: value,
});
}}
/>
<InputDoubleRange
values={rangeValues}
onChange={handleSliderChange}
minValue={rangeValues[0] || 1}
maxValue={rangeValues[1] || 2}
showLabel={true}
step={1}
/>
<div className="flex space-x-2">
<Button
onClick={onGenerateImage}
className="w-1/2"
>
Generate
</Button>
<Button
onClick={returnTo4D}
disabled={!displayingComputedVolume}
className="w-1/2"
>
Return To 4D
</Button>
</div>
</div>
</>
);
};
GenerateVolume.propTypes = {
rangeValues: PropTypes.array.isRequired,
handleSliderChange: PropTypes.func.isRequired,
operationsUI: PropTypes.array.isRequired,
options: PropTypes.object.isRequired,
handleGenerateOptionsChange: PropTypes.func.isRequired,
onGenerateImage: PropTypes.func.isRequired,
returnTo4D: PropTypes.func.isRequired,
};
export default GenerateVolume;
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { useCine } from '@ohif/ui';
import { useCine } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { utilities as cstUtils } from '@cornerstonejs/tools';
File diff suppressed because it is too large. Load diff
+11 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.128",
"version": "3.12.0-beta.2",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -38,10 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.7.0",
"@ohif/core": "3.10.0-beta.128",
"@ohif/ui": "3.10.0-beta.128",
"dcmjs": "*",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/core": "3.12.0-beta.2",
"@ohif/ui": "3.12.0-beta.2",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -51,11 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.0",
"@cornerstonejs/core": "^3.7.0",
"@cornerstonejs/labelmap-interpolation": "^3.7.0",
"@cornerstonejs/polymorphic-segmentation": "^3.7.0",
"@cornerstonejs/tools": "^3.7.0",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/ai": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/labelmap-interpolation": "^3.32.5",
"@cornerstonejs/polymorphic-segmentation": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
@@ -2,6 +2,11 @@
width: 100%;
height: 100%; /* MUST have `height` to prevent resize infinite loop */
position: relative;
/* Prevent text selection on the entire viewport wrapper */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.cornerstone-viewport-element {
@@ -16,4 +21,26 @@
/* Prevents the entire page from getting larger
when the magnify tool is near the sides/corners of the page */
overflow: hidden;
/* Prevent text selection on the viewport element */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Prevent text selection on overlay elements */
.noselect {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Ensure all elements within viewport wrapper prevent text selection */
.viewport-wrapper * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
@@ -1,9 +1,8 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { AllInOneMenu, useViewportDialog } from '@ohif/ui';
import { MeasurementService, useViewportRef } from '@ohif/core';
import { useViewportDialog } from '@ohif/ui-next';
import type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
@@ -14,8 +13,6 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
@@ -23,11 +20,8 @@ import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewport
const STACK = 'stack';
/**
* Caches the jump to measurement operation, so that if display set is shown,
* it can jump to the measurement.
*/
let cacheJumpToMeasurementEvent;
// Cache for viewport dimensions, persists across component remounts
const viewportDimensions = new Map<string, { width: number; height: number }>();
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
@@ -46,7 +40,6 @@ const OHIFCornerstoneViewport = React.memo(
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
@@ -88,6 +81,7 @@ const OHIFCornerstoneViewport = React.memo(
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const viewportRef = useViewportRef(viewportId);
const {
displaySetService,
@@ -97,24 +91,56 @@ const OHIFCornerstoneViewport = React.memo(
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
customizationService,
measurementService,
} = servicesManager.services;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
const scrollbarHeight = `${elementRef.current.clientHeight - 10}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
const onResize = useCallback(
(entries: ResizeObserverEntry[]) => {
if (elementRef.current && entries?.length) {
const entry = entries[0];
const { width, height } = entry.contentRect;
const prevDimensions = viewportDimensions.get(viewportId) || { width: 0, height: 0 };
// Check if dimensions actually changed and then only resize if they have changed
const hasDimensionsChanged =
prevDimensions.width !== width || prevDimensions.height !== height;
if (width > 0 && height > 0 && hasDimensionsChanged) {
viewportDimensions.set(viewportId, { width, height });
// Perform resize operations
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}
},
[viewportId, elementRef, cornerstoneViewportService, setImageScrollBarHeight]
);
useEffect(() => {
const element = elementRef.current;
if (!element) {
return;
}
}, [elementRef]);
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
// Cleanup function
return () => {
resizeObserver.unobserve(element);
resizeObserver.disconnect();
};
}, [onResize]);
const cleanUpServices = useCallback(
viewportInfo => {
@@ -125,16 +151,8 @@ const OHIFCornerstoneViewport = React.memo(
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
segmentationService.clearSegmentationRepresentations(viewportId);
viewportActionCornersService.clear(viewportId);
},
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
[viewportId, segmentationService, syncGroupService, toolGroupService]
);
const elementEnabledHandler = useCallback(
@@ -202,6 +220,7 @@ const OHIFCornerstoneViewport = React.memo(
}
cornerstoneViewportService.disableElement(viewportId);
viewportRef.unregister();
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
@@ -263,14 +282,6 @@ const OHIFCornerstoneViewport = React.memo(
const presentations = getViewportPresentations(viewportId, viewportOptions);
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
@@ -288,91 +299,11 @@ const OHIFCornerstoneViewport = React.memo(
displaySetOptions,
presentations
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
};
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
const windowLevelActionMenu = customizationService.getCustomization(
'viewportActionMenu.windowLevelActionMenu'
);
const segmentationOverlay = customizationService.getCustomization(
'viewportActionMenu.segmentationOverlay'
);
if (windowLevelActionMenu?.enabled) {
viewportActionCornersService.addComponent({
viewportId,
id: 'windowLevelActionMenu',
component: getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location: windowLevelActionMenu.location,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
}),
location: windowLevelActionMenu.location,
});
}
if (segmentationOverlay?.enabled) {
viewportActionCornersService.addComponent({
viewportId,
id: 'segmentation',
component: getViewportDataOverlaySettingsMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location: segmentationOverlay.location,
}),
location: segmentationOverlay.location,
});
}
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
const Notification = customizationService.getCustomization('ui.notificationComponent');
return (
@@ -383,9 +314,12 @@ const OHIFCornerstoneViewport = React.memo(
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
data-viewportid={viewportId}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
if (el) {
viewportRef.register(el);
}
}}
></div>
<CornerstoneOverlays
@@ -427,101 +361,6 @@ const OHIFCornerstoneViewport = React.memo(
areEqual
);
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
props => {
cacheJumpToMeasurementEvent = props;
const { viewportId: jumpId, measurement, isConsumed } = props;
if (!measurement || isConsumed) {
return;
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport =
cornerstoneViewportService.getViewportIdToJump(jumpId, {
displaySetInstanceUID: measurement.displaySetInstanceUID,
...measurement.metadata,
referencedImageId:
measurement.referencedImageId || measurement.metadata?.referencedImageId,
});
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
return;
}
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
}
);
return unsubscribe;
}
// Check if there is a queued jumpToMeasurement event
function _checkForCachedJumpToMeasurementEvents(
elementRef,
viewportId,
displaySets,
servicesManager
) {
if (!cacheJumpToMeasurementEvent) {
return;
}
if (cacheJumpToMeasurementEvent.isConsumed) {
cacheJumpToMeasurementEvent = null;
return;
}
const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
if (!displaysUIDs?.length) {
return;
}
// Jump to measurement if the measurement exists
const { measurement } = cacheJumpToMeasurementEvent;
if (measurement && elementRef) {
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
}
}
}
function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesManager) {
const { viewportGridService } = servicesManager.services;
const targetElement = targetElementRef.current;
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
// to set it properly
// setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportId(viewportId);
const enabledElement = getEnabledElement(targetElement);
if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
const { metadata } = measurement;
if (!viewport.isReferenceViewable(metadata, WITH_NAVIGATION)) {
console.log("Reference isn't viewable, postponing until updated");
return;
}
try {
viewport.setViewReference(metadata);
} catch (e) {
console.warn('Unable to apply', metadata, e);
}
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Jump to measurement consumed, remove.
cacheJumpToMeasurementEvent?.consume?.();
cacheJumpToMeasurementEvent = null;
}
}
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
@@ -582,22 +421,18 @@ OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
return false;
}
@@ -605,12 +440,10 @@ function areEqual(prevProps, nextProps) {
nextProps.viewportOptions.viewportType &&
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
return false;
}
@@ -618,7 +451,6 @@ function areEqual(prevProps, nextProps) {
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
return false;
}
@@ -631,13 +463,11 @@ function areEqual(prevProps, nextProps) {
);
if (!foundDisplaySet) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
return false;
}
@@ -645,7 +475,6 @@ function areEqual(prevProps, nextProps) {
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
return false;
}
}
@@ -655,16 +484,4 @@ function areEqual(prevProps, nextProps) {
return true;
}
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport;
@@ -1,15 +1,17 @@
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 { metaData, Enums, utilities, eventTarget } from '@cornerstonejs/core';
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui';
import { ViewportOverlay } from '@ohif/ui-next';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import './CustomizableViewportOverlay.css';
import { useViewportRendering } from '../../hooks';
const EPSILON = 1e-4;
const { formatPN } = utils;
@@ -67,6 +69,8 @@ function CustomizableViewportOverlay({
servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1);
const [annotationState, setAnnotationState] = useState(0);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const { imageIndex } = imageSliceData;
// Historical usage defined the overlays as separate items due to lack of
@@ -130,6 +134,20 @@ function CustomizableViewportOverlay({
};
}, [viewportId, viewportData, voi, element]);
const annotationModified = useCallback(evt => {
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
// Update the annotation state to trigger a re-render
setAnnotationState(prevState => prevState + 1);
}
}, []);
useEffect(() => {
eventTarget.addEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
return () => {
eventTarget.removeEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
};
}, [annotationModified]);
/**
* Updating the scale when the viewport changes its zoom
*/
@@ -170,6 +188,7 @@ function CustomizableViewportOverlay({
viewportId,
servicesManager,
customization: item,
isLight,
formatters: {
formatPN,
formatDate: formatDICOMDate,
@@ -206,6 +225,7 @@ function CustomizableViewportOverlay({
voi,
scale,
instanceNumber,
annotationState,
]
);
@@ -219,6 +239,7 @@ function CustomizableViewportOverlay({
instanceNumber,
viewportId,
toolGroupService,
isLight,
};
return (
@@ -241,6 +262,8 @@ function CustomizableViewportOverlay({
topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
color={isLight ? 'text-neutral-dark' : 'text-neutral-light'}
shadowClass={isLight ? 'shadow-light' : 'shadow-dark'}
/>
);
}
@@ -368,13 +391,14 @@ function OverlayItem(props) {
title={title}
>
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span>
<span className="ml-0 mr-2 shrink-0">{value}</span>
</div>
);
}
/**
* Window Level / Center Overlay item
* //
*/
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi;
@@ -387,10 +411,10 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
className="overlay-item flex flex-row"
style={{ color: customization?.color }}
>
<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>
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
<span className="mr-0.5 shrink-0 opacity-[0.70]">L:</span>
<span className="shrink-0">{windowCenter.toFixed(0)}</span>
</div>
);
}
@@ -404,7 +428,7 @@ function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }}
>
<span className="mr-1 shrink-0">Zoom:</span>
<span className="mr-0.5 shrink-0 opacity-[0.70]">Zoom:</span>
<span>{scale.toFixed(2)}x</span>
</div>
);
@@ -428,7 +452,7 @@ function InstanceNumberOverlayItem({
<span>
{instanceNumber !== undefined && instanceNumber !== null ? (
<>
<span className="mr-1 shrink-0">I:</span>
<span className="mr-0.5 shrink-0 opacity-[0.70]">I:</span>
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
</>
) : (
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui';
import { ImageScrollbar } from '@ohif/ui-next';
function CornerstoneImageScrollbar({
viewportData,
@@ -63,7 +63,7 @@ function CornerstoneImageScrollbar({
}
const { viewportType } = viewportData;
const eventId =
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
Enums.Events.IMAGE_RENDERED;
@@ -72,11 +72,11 @@ function CornerstoneImageScrollbar({
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds
setImageSliceData({
imageIndex: newImageIdIndex,
imageIndex: newImageIdIndex ?? imageIdIndex,
numberOfSlices,
});
};
@@ -3,27 +3,30 @@
--marker-height: 100px;
--scrollbar-width: 20px;
pointer-events: none;
font-size: 15px;
line-height: 18px;
}
.ViewportOrientationMarkers .orientation-marker {
position: absolute;
}
.ViewportOrientationMarkers .top-mid {
top: 0.6rem;
top: 0.38rem;
left: 50%;
transform: translateX(-50%);
}
.ViewportOrientationMarkers .left-mid {
top: 47%;
left: 5px;
top: 50%;
left: 0.38rem;
transform: translateY(-50%);
}
.ViewportOrientationMarkers .right-mid {
top: 47%;
top: 50%;
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
transform: translateY(-50%);
}
.ViewportOrientationMarkers .bottom-mid {
top: calc(100% - var(--marker-height) - 0.6rem);
left: 47%;
left: 50%;
transform: translateX(-50%);
}
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
display: flex;
@@ -1,11 +1,17 @@
import React, { useEffect, useState, useMemo } from 'react';
import React, { useEffect, useState, useMemo, useRef } from 'react';
import classNames from 'classnames';
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import {
metaData,
Enums,
Types,
getEnabledElement,
utilities as coreUtilities,
} from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
import { useViewportRendering } from '../../hooks';
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
function ViewportOrientationMarkers({
@@ -20,8 +26,34 @@ function ViewportOrientationMarkers({
const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const { cornerstoneViewportService } = servicesManager.services;
// Store initial viewUp and viewRight for volume viewports
const initialVolumeOrientationRef = useRef<{
initialViewUp: number[] | null;
initialViewRight: number[] | null;
}>({
initialViewUp: null,
initialViewRight: null,
});
useEffect(() => {
initialVolumeOrientationRef.current.initialViewUp = null;
initialVolumeOrientationRef.current.initialViewRight = null;
if (viewportData?.viewportType !== 'stack' && element && getEnabledElement(element)) {
const { viewport } = getEnabledElement(element);
const { viewUp, viewPlaneNormal } = viewport.getCamera();
const viewRight = vec3.create();
vec3.cross(viewRight, viewUp, viewPlaneNormal);
initialVolumeOrientationRef.current.initialViewUp = [...viewUp];
initialVolumeOrientationRef.current.initialViewRight = [...viewRight];
}
}, [element, viewportData]);
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { previousCamera, camera } = evt.detail;
@@ -79,14 +111,21 @@ function ViewportOrientationMarkers({
return '';
}
const { viewport } = getEnabledElement(element);
const { viewUp, viewPlaneNormal } = viewport.getCamera();
const viewRight = vec3.create();
vec3.cross(viewRight, viewUp, viewPlaneNormal);
columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]];
rowCosines = viewRight;
if (
initialVolumeOrientationRef.current.initialViewUp &&
initialVolumeOrientationRef.current.initialViewRight
) {
// Use initial orientation values for consistency, even as the camera changes
columnCosines = [
-initialVolumeOrientationRef.current.initialViewUp[0],
-initialVolumeOrientationRef.current.initialViewUp[1],
-initialVolumeOrientationRef.current.initialViewUp[2],
];
rowCosines = initialVolumeOrientationRef.current.initialViewRight;
} else {
console.warn('ViewportOrientationMarkers::No initial orientation values');
return '';
}
}
if (
@@ -119,8 +158,9 @@ function ViewportOrientationMarkers({
className={classNames(
'overlay-text',
`${m}-mid orientation-marker`,
'text-aqua-pale',
'text-[13px]',
isLight ? 'text-neutral-dark/70' : 'text-neutral-light/70',
isLight ? 'shadow-light' : 'shadow-dark',
'text-base',
'leading-5'
)}
key={`${m}-mid orientation-marker`}
@@ -136,6 +176,7 @@ function ViewportOrientationMarkers({
flipHorizontal,
orientationMarkers,
element,
isLight,
]);
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
@@ -157,6 +198,9 @@ function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertica
const oppositeRowString = invertOrientationStringLPS(rowString);
const oppositeColumnString = invertOrientationStringLPS(columnString);
// Round to 4 decimal places (after rotate right 3 times and flip horizontally -> rotation is 90.00000000000001)
rotation = Math.round(rotation * 10000) / 10000;
const markers = {
top: oppositeColumnString,
left: oppositeRowString,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,195 @@
import React from 'react';
import { useSystem } from '@ohif/core';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
import { ChevronDownIcon } from '@radix-ui/react-icons';
export type AccordionGrouping = {
[name: string]: unknown;
};
export type AccordionGroupProps = {
grouping: AccordionGrouping;
};
/**
* Searches for the required type from the provided allChildren list and
* renders them.
*/
export const CloneChildren = props => {
const { group, allChildren, children, childType, defaultTypes } = props;
const subType = group?.subType;
for (const child of allChildren) {
if (subType && child.props?.subType !== subType) {
continue;
}
if (childType && child.type === childType) {
return React.cloneElement(child, { ...props, children: child.props.children });
}
if (defaultTypes?.indexOf(child.type) === -1) {
return childType({ ...props, children: child });
}
}
if (!children) {
throw new Error(`No children defined for ${props.name} CloneChildren in group ${group?.name}`);
}
return React.cloneElement(children, props);
};
/** Used to exclude defaults */
const DEFAULT_TYPES = [GroupAccordion, Content, Trigger];
/**
* An AccordionGroup is a component that splits a set of items into different
* groups according to a set of grouping rules. It then puts the groups
* into a set of accordion folds selected from the body of the accordion group,
* looking for matching trigger/content sections according to the type definition
* in the group with first one found being used.
*
* This design allows for easy customization of the component by declaring grouping
* functions with default grouping setups and then only overriding the specific
* children needing to be changed. See the PanelMeasurement for some example
* possibilities of how to modify the default grouping, or the test-extension
* measurements panel for a practical, working example.
*/
export function AccordionGroup(props) {
const { grouping, items, children, sourceChildren, type } = props;
const childProps = useSystem();
let defaultValue = props.defaultValue;
const groups = grouping.groupingFunction(items, grouping, childProps);
if (!defaultValue) {
const defaultGroup = groups.values().find(group => group.isSelected);
defaultValue = defaultGroup?.key || defaultGroup?.title;
}
const valueArr =
(Array.isArray(defaultValue) && defaultValue) || (defaultValue && [defaultValue]) || [];
const sourceChildrenArr = sourceChildren ? React.Children.toArray(sourceChildren) : [];
const childrenArr = children ? React.Children.toArray(children) : [];
const allChildren = sourceChildrenArr.concat(childrenArr);
return (
<CloneChildren
allChildren={allChildren}
groups={groups}
childType={GroupAccordion}
grouping={grouping}
defaultValue={valueArr}
name={'grouping ' + grouping.name}
>
<DefaultAccordion name="DefaultAccordion" />
</CloneChildren>
);
}
function DefaultAccordion(props) {
const { groups, defaultValue, grouping, allChildren, asChild } = props;
if (!allChildren || !groups) {
return null;
}
if (Boolean(asChild)) {
return React.cloneElement(props.children, props);
}
return (
<Accordion
type={grouping.type || 'multiple'}
className="text-white"
defaultValue={defaultValue}
>
{[...groups.entries()].map(([key, group]) => {
return (
<AccordionItem
key={group.key + '-i'}
value={group.key}
>
<CloneChildren
allChildren={allChildren}
group={group}
childType={Trigger}
name="AccordionGroup.Trigger"
/>
<CloneChildren
allChildren={allChildren}
group={group}
childType={Content}
defaultTypes={DEFAULT_TYPES}
name="AccordionGroup.Content"
/>
</AccordionItem>
);
})}
</Accordion>
);
}
function GroupAccordion(props) {
const { groups, children } = props;
if (!groups) {
return null;
}
return [...groups.values()].map(group =>
React.cloneElement(children, {
...props,
children: children.props.children,
group,
...group,
key: group.title,
})
);
}
function Content(props) {
const { children, asChild, ...childProps } = props;
const { group } = props;
Object.assign(childProps, group);
if (!group) {
return null;
}
if (asChild) {
return React.cloneElement(children, { ...group, ...props, children: children.props.children });
}
return (
<AccordionContent>
{React.cloneElement(children, { ...group, ...props, children: children.props.children })}
</AccordionContent>
);
}
function Trigger(props) {
const { children, asChild, ...childProps } = props;
const { group } = props;
Object.assign(childProps, group);
if (!group) {
return null;
}
if (asChild) {
return React.cloneElement(children, childProps);
}
return (
<AccordionTrigger
value={group.value}
asChild={true}
>
<div>
{React.cloneElement(children, childProps)}
<ChevronDownIcon
key="chevronDown"
className="text-primary h-4 w-4 shrink-0 transition-transform duration-200"
/>
</div>
</AccordionTrigger>
);
}
AccordionGroup.Content = Content;
AccordionGroup.Trigger = Trigger;
AccordionGroup.Accordion = GroupAccordion;
export default AccordionGroup;
@@ -0,0 +1,4 @@
export * from './AccordionGroup';
import AccordionGroup from './AccordionGroup';
export default AccordionGroup;
@@ -0,0 +1,149 @@
import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useViewportRendering } from '../../hooks';
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
import classNames from 'classnames';
const mouseNearControlsRanges = {
[ButtonLocation.TopMiddle]: { minX: 0, minY: 0, maxX: 1, maxY: 0.1 },
[ButtonLocation.BottomMiddle]: { minX: 0, minY: 0.9, maxX: 1, maxY: 1 },
[ButtonLocation.LeftMiddle]: { minX: 0, minY: 0, maxX: 0.1, maxY: 1 },
[ButtonLocation.RightMiddle]: { minX: 0.9, minY: 0, maxX: 1, maxY: 1 },
};
const getFlexDirectionClassName = (location: ButtonLocation) =>
location === ButtonLocation.LeftMiddle || location === ButtonLocation.RightMiddle
? 'flex-col'
: 'flex-row';
function AdvancedRenderingControls({
viewportId,
location,
buttonSection,
}: {
viewportId: string;
location: number;
buttonSection: string;
}) {
const {
onInteraction,
toolbarButtons,
isItemOpen,
isItemLocked,
openItem,
closeItem,
toggleLock,
} = useToolbar({
buttonSection,
});
const mousePosition = useViewportMousePosition(viewportId);
const [isMouseNearControls, setIsMouseNearControls] = useState(false);
const [showAllIcons, setShowAllIcons] = useState(true);
const firstMountRef = useRef(true);
const { hasColorbar } = useViewportRendering(viewportId);
const [isAnItemOpen, setIsAnItemOpen] = useState(false);
useEffect(() => {
if (firstMountRef.current) {
firstMountRef.current = false;
const timer = setTimeout(() => {
setShowAllIcons(false);
}, 3000);
return () => clearTimeout(timer);
}
}, []);
useEffect(() => {
if (!showAllIcons && mousePosition.isInViewport) {
const mouseHoverLocation = mouseNearControlsRanges[location];
if (mousePosition.isWithinNormalizedBox(mouseHoverLocation)) {
setIsMouseNearControls(true);
} else {
setIsMouseNearControls(false);
}
}
}, [location, mousePosition, showAllIcons]);
const handleOnItemOpen = useCallback(
(id, viewportId) => {
openItem(id, viewportId);
setIsAnItemOpen(true);
},
[openItem, setIsAnItemOpen]
);
const handleOnItemClose = useCallback(
(id, viewportId) => {
closeItem(id, viewportId);
setIsAnItemOpen(false);
},
[closeItem, setIsAnItemOpen]
);
if (!toolbarButtons?.length) {
return null;
}
if (!hasColorbar) {
return null;
}
return (
<div className={classNames('flex gap-2', getFlexDirectionClassName(location))}>
{toolbarButtons.map(toolDef => {
if (!toolDef) {
return null;
}
const { id, Component, componentProps } = toolDef;
// Enhanced props with state and actions - respecting viewport specificity
const enhancedProps = {
...componentProps,
isOpen: isItemOpen(id, viewportId),
isLocked: isItemLocked(id, viewportId),
onOpen: () => handleOnItemOpen(id, viewportId),
onClose: () => handleOnItemClose(id, viewportId),
onToggleLock: () => toggleLock(id, viewportId),
viewportId,
};
const tool = (
<Component
key={id}
id={id}
location={location}
onInteraction={args => {
onInteraction({
...args,
itemId: id,
viewportId,
});
}}
{...enhancedProps}
/>
);
// Always show all icons on first mount for 3 seconds
// After that, always show Colorbar, show others only when mouse is at bottom
const shouldBeVisible =
isAnItemOpen || showAllIcons || id === 'Colorbar' || isMouseNearControls;
return (
<div
key={id}
className={shouldBeVisible ? 'opacity-100' : 'pointer-events-none opacity-0'}
style={{ transition: 'opacity 0.2s ease-in-out' }}
>
{tool}
</div>
);
})}
</div>
);
}
export default AdvancedRenderingControls;
@@ -0,0 +1 @@
export { default } from './AdvancedRenderingControls';
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { useCine } from '@ohif/ui';
import { useCine } from '@ohif/ui-next';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { useAppConfig } from '@state';
@@ -244,4 +244,4 @@ function RenderCinePlayer({
);
}
export default WrappedCinePlayer;
export default WrappedCinePlayer;
@@ -259,22 +259,24 @@ function DicomUploadProgress({
}, [getPercentCompleteRounded, percentComplete]);
/**
* Gets the css style for the 'n of m' (files completed) text. The only css attribute
* of the style is width such that the 'n of m' is always a fixed width and thus
* as each file completes uploading the text on screen does not constantly shift
* left and right.
* Gets the CSS style for the 'n of m' (files completed) text.
* The width changes according to numFilesCompleted and can vary,
* e.g. "1 of 200", "10 of 200", "100 of 200" all have differents width.
*/
const getNofMFilesStyle = useCallback(() => {
// the number of digits accounts for the digits being on each side of the ' of '
const numDigits = 2 * dicomFileUploaderArr.length.toString().length;
// the number of digits + 2 spaces and 2 characters for ' of '
const numChars = numDigits + 4;
const numDigits =
numFilesCompleted.toString().length + dicomFileUploaderArr.length.toString().length;
// The number of digits + 3 additional characters (accounts for ' of ').
// Even though intuitively 4 should be better, this is the most accurate width.
// The font may play a part in this discrepancy.
const numChars = numDigits + 3;
return { width: `${numChars}ch` };
}, []);
}, [numFilesCompleted]);
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
return (
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg">
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg text-white">
{numFilesCompleted === dicomFileUploaderArr.length ? (
<>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${dicomFileUploaderArr.length} ${
@@ -290,21 +292,24 @@ function DicomUploadProgress({
</>
) : (
<>
<div className="flex flex-wrap">
<span
style={getNofMFilesStyle()}
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-right')}
>
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}&nbsp;
</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'}</span>
<br />
<span>
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
</span>
</div>
<span
style={getNofMFilesStyle()}
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-end')}
>
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}&nbsp;
</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'}&nbsp;</span>
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
</span>
<span
className={classNames(
NO_WRAP_ELLIPSIS_CLASS_NAMES,
'text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
)}
className={
'text-primary hover:text-primary-lightactive:text-aqua-pale ml-auto cursor-pointer whitespace-nowrap'
}
onClick={cancelAllUploads}
>
Cancel All Uploads
@@ -354,7 +359,7 @@ function DicomUploadProgress({
></ProgressLoadingBar>
</div>
<div className="ml-1 flex w-24 items-center">
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
<div className="w-10 text-right text-foreground">{`${getPercentCompleteRounded()}%`}</div>
{getShowFailedOnlyIconComponent()}
</div>
</>
@@ -75,7 +75,7 @@ const DicomUploadProgressItem = memo(
<div className="self-top flex w-0 shrink grow flex-col gap-1">
<div className="flex gap-4">
<div className="flex w-6 shrink-0 items-center justify-center">{getStatusIcon()}</div>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-white">
{dicomFileUploader.getFileName()}
</div>
</div>
@@ -89,7 +89,7 @@ const DicomUploadProgressItem = memo(
)}
<div className="ml-auto flex cursor-pointer">
<Icons.Close
className="text-primary-active self-center"
className="text-primary self-center"
onClick={cancelUpload}
/>
</div>
@@ -0,0 +1,70 @@
import React from 'react';
import { Accordion, AccordionContent, AccordionItem } from '@ohif/ui-next';
import PanelAccordionTrigger from './PanelAccordionTrigger';
import MeasurementsMenu from './MeasurementsMenu';
import { useSystem } from '@ohif/core';
export function MeasurementItem(props) {
const { index, item } = props;
return (
<PanelAccordionTrigger
count={index + 1}
text={item.toolName || item.label || item.title}
colorHex="#f00"
isActive={item.isSelected}
menu={MeasurementsMenu}
group={{ items: [item], onClick: props.onClick }}
/>
);
}
export default function MeasurementAccordion(props) {
const { items } = props;
const system = useSystem();
const onClick = (e, group) => {
const { items } = group;
// Just jump to the first measurement in the set, and mark that one as active
// with the set of items.
system.commandsManager.run('jumpToMeasurement', {
uid: items[0].uid,
displayMeasurements: items,
group,
});
};
return (
<Accordion
type="multiple"
className="flex-shrink-0 overflow-hidden"
>
{items.map((item, index) => {
const { displayText: details = {} } = item;
return (
<AccordionItem
key={`measurementAccordion:${item.uid}`}
value={item.uid}
>
<MeasurementItem
item={item}
key={`measurementItem:${item.uid}`}
index={index}
onClick={onClick}
/>
<AccordionContent key={`measurementContent:${item.uid}`}>
<div className="ml-7 px-2 py-2">
<div className="text-secondary-foreground flex items-center gap-1 text-base leading-normal">
{details.primary?.length > 0 &&
details.primary.map((detail, index) => (
<span key={`details:${item.uid}:${index}`}>{detail}</span>
))}
</div>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
);
}
@@ -0,0 +1,30 @@
import React from 'react';
import { MeasurementTable } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
/**
* This is a measurement table that is designed to be nested inside
* the accordion groups.
*/
export default function MeasurementTableNested(props) {
const { title, items, group, customHeader } = props;
const { commandsManager } = useSystem();
const onAction = (e, command, uid) => {
commandsManager.run(command, { uid, annotationUID: uid, displayMeasurements: items });
};
return (
<MeasurementTable
title={title ? title : `Measurements`}
data={items}
onAction={onAction}
{...group}
key={group.key}
>
<MeasurementTable.Header key="measurementTableHeader">
{customHeader && group.isFirst && customHeader({ ...props, items: props.allItems })}
</MeasurementTable.Header>
<MeasurementTable.Body key="measurementTableBody" />
</MeasurementTable>
);
}
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Icons,
} from '@ohif/ui-next';
export function MeasumentsMenu(props) {
const { group, classNames } = props;
if (!group.items?.length) {
console.log('No items to iterate', group.items);
return null;
}
const { items } = group;
const [item] = items;
const { isSelected, isVisible } = item;
const system = useSystem();
const onAction = (event, command, args?) => {
const uid = items.map(item => item.uid);
// Some commands use displayMeasurements and some use items
system.commandsManager.run(command, {
...args,
uid,
displayMeasurements: items,
items,
event,
});
};
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
return (
<div className={`relative ml-2 inline-flex items-center space-x-1 ${classNames}`}>
{/* Visibility Toggle Icon */}
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || !isVisible ? 'opacity-100' : 'opacity-50 group-hover:opacity-100'
}`}
aria-label={isVisible ? 'Hide' : 'Show'}
onClick={e => {
e.stopPropagation();
onAction(e, ['jumpToMeasurement', 'toggleVisibilityMeasurement']);
}}
>
{isVisible ? <Icons.Hide className="h-6 w-6" /> : <Icons.Show className="h-6 w-6" />}
</Button>
{/* Actions Dropdown Menu */}
<DropdownMenu onOpenChange={open => setIsDropdownOpen(open)}>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className={`h-6 w-6 transition-opacity ${
isSelected || isDropdownOpen ? 'opacity-100' : 'opacity-50 group-hover:opacity-100'
}`}
aria-label="Actions"
onClick={e => e.stopPropagation()} // Prevent row selection on button click
>
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={e => onAction(e, 'removeMeasurement')}>
<Icons.Delete className="text-foreground" />
<span className="pl-2">Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export default MeasumentsMenu;
@@ -0,0 +1,82 @@
import React from 'react';
import AccordionGroup from './AccordionGroup';
import { utils } from '@ohif/core';
import MeasurementTableNested from './MeasurementTableNested';
const { filterNot, filterAdditionalFindings } = utils.MeasurementFilters;
export const MeasurementOrAdditionalFindingSets = [
{
title: 'Measurements',
filter: filterNot(filterAdditionalFindings),
},
{
title: 'Additional Findings',
filter: filterAdditionalFindings,
},
];
/**
* Groups measurements by study in order to allow display and saving by study
* @param {Object} servicesManager
*/
export const groupByNamedSets = (items, grouping) => {
const groups = new Map();
const { namedSets } = grouping;
for (const namedSet of namedSets) {
const name = namedSet.id || namedSet.title;
groups.set(name, {
...grouping,
...namedSet,
items: [],
isFirst: groups.size === 0,
title: name,
key: name,
});
}
items.forEach(item => {
for (const namedSet of namedSets) {
if (namedSet.filter(item)) {
const name = namedSet.id || namedSet.title;
groups.get(name).items.push(item);
return;
}
}
});
for (const namedSet of namedSets) {
const name = namedSet.id || namedSet.title;
if (!groups.get(name).items.length) {
groups.delete(name);
}
}
return groups;
};
export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
const { items, children, grouping = {}, customHeader, group, actions } = props;
return (
<AccordionGroup
grouping={{
groupingFunction: groupByNamedSets,
name: 'measurementsOrAdditional',
namedSets: MeasurementOrAdditionalFindingSets,
StudyInstanceUID: group?.StudyInstanceUID,
...grouping,
}}
items={items}
sourceChildren={children}
>
<AccordionGroup.Accordion noWrapper="true">
<MeasurementTableNested
customHeader={customHeader}
allItems={items}
actions={actions}
/>
</AccordionGroup.Accordion>
</AccordionGroup>
);
}
export default MeasurementsOrAdditionalFindings;
@@ -0,0 +1,171 @@
import React, { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ViewportActionButton } from '@ohif/ui-next';
import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
/**
* ModalityLoadBadge displays the status and actionable buttons for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) or when tracking measurements
*/
function ModalityLoadBadge({ viewportId }: { viewportId: string }) {
const { commandsManager } = useSystem();
const { t } = useTranslation('Common');
const loadStr = t('LOAD');
const { isTracked, isLocked } = useMeasurementTracking({ viewportId });
const { backgroundDisplaySet } = useViewportDisplaySets(viewportId);
const [specialDisplaySet, setSpecialDisplaySet] = useState(null);
const allDisplaySets = useMemo(() => {
return [backgroundDisplaySet].filter(Boolean);
}, [backgroundDisplaySet]);
useEffect(() => {
const displaySet = allDisplaySets.find(ds => ds.isOverlayDisplaySet || ds?.Modality === 'SR');
setSpecialDisplaySet(displaySet || null);
}, [allDisplaySets]);
const statusInfo = useMemo(() => {
if (isTracked && !specialDisplaySet) {
return {
type: 'SR',
isHydrated: true,
isTracked,
displaySet: null,
tooltip: 'Currently tracking measurements in this viewport',
};
}
if (!specialDisplaySet) {
return {
type: null,
};
}
const type = specialDisplaySet.Modality;
const isHydrated = specialDisplaySet.isHydrated || false;
let tooltip = null;
const isRehydratable = specialDisplaySet.isRehydratable || false;
if (type === 'SEG') {
tooltip = isHydrated
? 'This Segmentation is loaded in the segmentation panel'
: 'Click LOAD to load segmentation.';
} else if (type === 'RTSTRUCT') {
tooltip = isHydrated
? 'This RTSTRUCT is loaded in the segmentation panel'
: 'Click LOAD to load RTSTRUCT.';
} else if (type === 'SR') {
if (!isRehydratable) {
tooltip = 'This structured report is not compatible with this application.';
} else if (isRehydratable && isLocked) {
tooltip =
'This structured report is currently read-only because you are tracking measurements in another viewport.';
} else {
tooltip = `Click ${loadStr} to restore measurements.`;
}
}
return {
type,
isHydrated,
displaySet: specialDisplaySet,
isRehydratable,
tooltip,
};
}, [specialDisplaySet, loadStr, isLocked, isTracked]);
// Nothing to show if there's no special display set type or tracking
if (!statusInfo.type && !isTracked) {
return null;
}
const StatusIcon = () => {
if (statusInfo.type === 'SR') {
if (statusInfo.isRehydratable && !isLocked) {
return (
<Icons.ByName
className="text-muted-foreground h-4 w-4"
name="status-untracked"
/>
);
}
if (statusInfo.isRehydratable && isLocked) {
return (
<Icons.ByName
name="status-locked"
className="h-4 w-4"
/>
);
} else {
return (
<Icons.ByName
name="status-alert"
className="h-4 w-4"
/>
);
}
} else {
return <Icons.StatusUntracked className="h-4 w-4" />;
}
};
const StatusArea = () => {
if (!statusInfo.isHydrated) {
return (
<div
data-cy={`ModalityLoadBadge-${viewportId}`}
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">{statusInfo.type}</span>
</div>
{/* We don't show the load button for SRs because we handle it in the SR extension
via the tracked measurement context that works with state machine, this is not a regression right now */}
{statusInfo.type !== 'SR' && (
<ViewportActionButton
onInteraction={() => {
commandsManager.runCommand('hydrateSecondaryDisplaySet', {
displaySet: statusInfo.displaySet,
viewportId,
});
}}
>
{loadStr}
</ViewportActionButton>
)}
</div>
);
}
return null;
};
return (
<>
{statusInfo.tooltip && (
<Tooltip>
<TooltipTrigger asChild>
<span>
<StatusArea />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<div>{statusInfo.tooltip}</div>
</TooltipContent>
</Tooltip>
)}
{!statusInfo.tooltip && <StatusArea />}
</>
);
}
export default ModalityLoadBadge;
@@ -0,0 +1 @@
export { default } from './ModalityLoadBadge';
@@ -0,0 +1,151 @@
import React, { useCallback, useState } from 'react';
import { ViewportActionArrows } from '@ohif/ui-next';
import { useSystem } from '@ohif/core/src';
import { utils } from '../..';
import { useViewportSegmentations } from '../../hooks';
import { useMeasurementTracking } from '../../hooks/useMeasurementTracking';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
/**
* NavigationComponent provides navigation controls for viewports containing
* special displaySets (SR, SEG, RTSTRUCT) to navigate between segments or measurements
*/
function NavigationComponent({ viewportId }: { viewportId: string }) {
const { servicesManager } = useSystem();
const { segmentationService, cornerstoneViewportService, measurementService } =
servicesManager.services;
// Get tracking information
const { isTracked, trackedMeasurementUIDs } = useMeasurementTracking({ viewportId });
const { viewportDisplaySets } = useViewportDisplaySets(viewportId);
const [measurementSelected, setMeasurementSelected] = useState(0);
const isSRDisplaySet = viewportDisplaySets.some(displaySet => displaySet?.Modality === 'SR');
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
// Get segmentation information
const { segmentationsWithRepresentations } = useViewportSegmentations({
viewportId,
});
const hasSegmentations = segmentationsWithRepresentations.length > 0;
// prefer segment navigation if available
const navigationMode = hasSegmentations
? 'segment'
: isSRDisplaySet
? 'measurement'
: isTracked
? 'measurement'
: null;
const handleMeasurementNavigation = useCallback(
(direction: number) => {
const measurementDisplaySet = viewportDisplaySets.find(
displaySet => displaySet?.Modality === 'SR'
);
if (measurementDisplaySet) {
const measurements = measurementDisplaySet.measurements;
if (measurements.length <= 0) {
return;
}
const newIndex = getNextIndex(measurementSelected, direction, measurements.length);
setMeasurementSelected(newIndex);
const measurement = measurements[newIndex];
cornerstoneViewport.setViewReference({
referencedImageId: measurement.imageId,
});
return;
}
if (isTracked && trackedMeasurementUIDs.length > 0) {
const newIndex = getNextIndex(
measurementSelected,
direction,
trackedMeasurementUIDs.length
);
setMeasurementSelected(newIndex);
measurementService.jumpToMeasurement(viewportId, trackedMeasurementUIDs[newIndex]);
}
},
[
viewportId,
cornerstoneViewport,
measurementSelected,
measurementService,
isTracked,
trackedMeasurementUIDs,
viewportDisplaySets,
]
);
const handleSegmentNavigation = useCallback(
(direction: number) => {
if (!segmentationsWithRepresentations.length) {
return;
}
const segmentationId = segmentationsWithRepresentations[0].segmentation.segmentationId;
utils.handleSegmentChange({
direction,
segmentationId,
viewportId,
selectedSegmentObjectIndex: 0,
segmentationService,
});
},
[segmentationsWithRepresentations, viewportId, segmentationService]
);
// Handle navigation between segments/measurements
const handleNavigate = useCallback(
(direction: number) => {
if (navigationMode === 'segment') {
handleSegmentNavigation(direction);
} else if (navigationMode === 'measurement') {
handleMeasurementNavigation(direction);
}
},
[navigationMode, handleSegmentNavigation, handleMeasurementNavigation]
);
// Only render if we have a navigation mode
if (!navigationMode) {
return null;
}
return (
<ViewportActionArrows
onArrowsClick={handleNavigate}
className="h-6"
/>
);
}
/**
* Calculate the next index with circular navigation support
* @param currentIndex Current index position
* @param direction Direction of movement (1 for next, -1 for previous)
* @param totalItems Total number of items to navigate through
* @returns The next index with wrap-around support
*/
function getNextIndex(currentIndex: number, direction: number, totalItems: number): number {
if (totalItems <= 0) {
return 0;
}
// Use modulo to handle circular navigation
let nextIndex = (currentIndex + direction) % totalItems;
// Handle negative index when going backwards from index 0
if (nextIndex < 0) {
nextIndex = totalItems - 1;
}
return nextIndex;
}
export default NavigationComponent;
@@ -0,0 +1,3 @@
import NavigationComponent from './NavigationComponent';
export default NavigationComponent;
@@ -1,25 +1,94 @@
import React from 'react';
import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider';
import { useSystem } from '@ohif/core';
import React, { memo } from 'react';
import { ViewportActionCorners, IconPresentationProvider, ToolButton } from '@ohif/ui-next';
import { Toolbar } from '@ohif/extension-default/src/Toolbar/Toolbar';
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
import { useViewportHover } from '../hooks';
export type OHIFViewportActionCornersProps = {
viewportId: string;
};
function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) {
const { servicesManager } = useSystem();
const [viewportActionCornersState] = useViewportActionCornersContext();
const ViewportActionCorners =
servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner');
if (!viewportActionCornersState[viewportId]) {
function OHIFViewportActionCornersComponent({ viewportId }: OHIFViewportActionCornersProps) {
// Use the viewport hover hook to track if viewport is hovered or active
const { isHovered, isActive } = useViewportHover(viewportId);
const shouldShowCorners = isHovered || isActive;
if (!shouldShowCorners) {
return null;
}
return (
<ViewportActionCorners
cornerComponents={viewportActionCornersState[viewportId]}
></ViewportActionCorners>
<IconPresentationProvider
size="medium"
IconContainer={ToolButton}
containerProps={{
size: 'tiny',
className: 'font-normal text-primary hover:bg-primary/25',
}}
>
<ViewportActionCorners.Container>
<ViewportActionCorners.TopLeft>
<Toolbar
buttonSection="viewportActionMenu.topLeft"
viewportId={viewportId}
location={ButtonLocation.TopLeft}
/>
</ViewportActionCorners.TopLeft>
<ViewportActionCorners.TopMiddle>
<Toolbar
buttonSection="viewportActionMenu.topMiddle"
viewportId={viewportId}
location={ButtonLocation.TopMiddle}
/>
</ViewportActionCorners.TopMiddle>
<ViewportActionCorners.TopRight>
<Toolbar
buttonSection="viewportActionMenu.topRight"
viewportId={viewportId}
location={ButtonLocation.TopRight}
/>
</ViewportActionCorners.TopRight>
<ViewportActionCorners.LeftMiddle>
<Toolbar
buttonSection="viewportActionMenu.leftMiddle"
viewportId={viewportId}
location={ButtonLocation.LeftMiddle}
/>
</ViewportActionCorners.LeftMiddle>
<ViewportActionCorners.RightMiddle>
<Toolbar
buttonSection="viewportActionMenu.rightMiddle"
viewportId={viewportId}
location={ButtonLocation.RightMiddle}
/>
</ViewportActionCorners.RightMiddle>
<ViewportActionCorners.BottomLeft>
<Toolbar
buttonSection="viewportActionMenu.bottomLeft"
viewportId={viewportId}
location={ButtonLocation.BottomLeft}
/>
</ViewportActionCorners.BottomLeft>
<ViewportActionCorners.BottomMiddle>
<Toolbar
buttonSection="viewportActionMenu.bottomMiddle"
viewportId={viewportId}
location={ButtonLocation.BottomMiddle}
/>
</ViewportActionCorners.BottomMiddle>
<ViewportActionCorners.BottomRight>
<Toolbar
buttonSection="viewportActionMenu.bottomRight"
viewportId={viewportId}
location={ButtonLocation.BottomRight}
/>
</ViewportActionCorners.BottomRight>
</ViewportActionCorners.Container>
</IconPresentationProvider>
);
}
const OHIFViewportActionCorners = memo(OHIFViewportActionCornersComponent);
export default OHIFViewportActionCorners;
@@ -0,0 +1,54 @@
import React from 'react';
import { Numeric } from '@ohif/ui-next';
import { useViewportDisplaySets, useViewportRendering } from '../../hooks';
interface OpacityMenuProps {
viewportId: string;
className?: string;
}
function OpacityMenu({ viewportId, className }: OpacityMenuProps) {
const { backgroundDisplaySet, foregroundDisplaySets } = useViewportDisplaySets(viewportId, {
includeForeground: true,
includeBackground: true,
});
const { opacityLinear, setOpacityLinear } = useViewportRendering(viewportId, {
displaySetInstanceUID: foregroundDisplaySets[0].displaySetInstanceUID,
});
const opacityValue = opacityLinear !== undefined ? opacityLinear : 0;
const backgroundModality = backgroundDisplaySet?.Modality || '';
const foregroundModality = foregroundDisplaySets[0]?.Modality || '';
return (
<div className={className}>
<div className="bg-popover w-72 rounded-lg p-3">
<div className="mb-2 flex items-center justify-center">
<span className="text-muted-foreground text-base">Opacity</span>
</div>
<div className="">
<Numeric.Container
mode="singleRange"
value={opacityValue}
onChange={(val: number | [number, number]) => {
if (typeof val === 'number') {
setOpacityLinear(val);
}
}}
min={0}
max={1}
step={0.0001}
>
<div className="flex items-center">
<span className="text-foreground mr-2 text-sm">{backgroundModality}</span>
<Numeric.SingleRange showNumberInput={false} />
<span className="text-foreground ml-2 text-sm">{foregroundModality}</span>
</div>
</Numeric.Container>
</div>
</div>
</div>
);
}
export default OpacityMenu;
@@ -0,0 +1,85 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useIconPresentation,
} from '@ohif/ui-next';
import OpacityMenu from './OpacityMenu';
type OpacityMenuWrapperProps = {
viewportId: string;
location: string;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
disabled?: boolean;
};
export function OpacityMenuWrapper(props: OpacityMenuWrapperProps): ReactNode {
const { viewportId, location, isOpen = false, onOpen, onClose, disabled, ...rest } = props;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const handleOpenChange = (openState: boolean) => {
if (openState) {
onOpen?.();
} else {
onClose?.();
}
};
const { servicesManager } = useSystem();
const { toolbarService } = servicesManager.services;
const { align, side } = toolbarService.getAlignAndSide(location);
const Icon = <Icons.Opacity className={iconClassName} />;
return (
<Popover
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
icon="Opacity"
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<OpacityMenu
className="w-full"
viewportId={viewportId}
/>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,2 @@
export { OpacityMenuWrapper } from './OpacityMenuWrapper';
export { default as OpacityMenu } from './OpacityMenu';
@@ -0,0 +1,48 @@
import React from 'react';
import { AccordionTrigger, ColorCircle } from '@ohif/ui-next';
import { ChevronDownIcon } from '@radix-ui/react-icons';
function onClickDefault(e) {
const { group, onClick = group?.onClick } = this;
if (!onClick) {
console.log('No onClick function', group);
return;
}
console.log('onClickDefault');
e.preventDefault();
e.stopPropagation();
onClick(e, group);
return false;
}
export default function PanelAccordionTrigger(props) {
const { marginLeft = 8, isActive = false, colorHex, count, text, menu: Menu = null } = props;
return (
<AccordionTrigger
style={{ marginLeft: `${marginLeft}px`, padding: 0 }}
asChild={true}
>
<div className={`inline-flex text-base ${isActive ? 'bg-popover' : 'bg-muted'} flex-grow`}>
<button onClick={onClickDefault.bind(props)}>
<span
className={`inline-flex rounded-l border-r border-black ${isActive ? 'bg-highlight' : 'bg-muted'}`}
>
{count !== undefined ? <span className="px-2">{count}</span> : null}
{colorHex && <ColorCircle colorHex={colorHex} />}
</span>
<span>{text}</span>
</button>
{Menu && (
<Menu
{...props}
classNames="justify-end flex-grow"
/>
)}
<ChevronDownIcon className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
</div>
</AccordionTrigger>
);
}
@@ -0,0 +1,26 @@
import React from 'react';
// should be used in a Select component
const SelectItemWithModality = ({
displaySet,
showModality = true,
dataCY = `${displaySet.label}-${displaySet.Modality}`,
}: {
displaySet: AppTypes.DisplaySet;
showModality?: boolean;
dataCY?: string;
}): JSX.Element => (
<div
className="flex w-[90%] items-center justify-between"
data-cy={dataCY}
>
<span className="text-foreground truncate text-base">{displaySet.label}</span>
{showModality && displaySet.Modality && (
<span className="text-muted-foreground flex-shrink-0 whitespace-nowrap text-xs">
{displaySet.Modality}
</span>
)}
</div>
);
export default SelectItemWithModality;
@@ -0,0 +1,90 @@
import React from 'react';
import { useActiveViewportDisplaySets, useSystem } from '@ohif/core';
import AccordionGroup from './AccordionGroup';
import PanelAccordionTrigger from './PanelAccordionTrigger';
import MeasurementItems from './MeasurementItems';
import MeasurementsMenu from './MeasurementsMenu';
/**
* Groups measurements by study in order to allow display and saving by study
* @param {Object} servicesManager
*/
export const groupByDisplaySet = (items, grouping, childProps) => {
const groups = new Map();
const { displaySetService } = childProps.servicesManager.services;
const { activeDisplaySetInstanceUID } = grouping;
items.forEach(item => {
const { displaySetInstanceUID } = item;
if (!groups.has(displaySetInstanceUID)) {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
groups.set(displaySetInstanceUID, {
header: null,
isSelected: displaySetInstanceUID == activeDisplaySetInstanceUID,
...grouping,
items: [],
key: displaySetInstanceUID,
title: 'Series Measurements',
displaySet,
});
}
groups.get(displaySetInstanceUID).items.push(item);
});
return groups;
};
export function SeriesMeasurementTrigger(props) {
const { group, isSelected, displaySet, menu } = props;
const { SeriesNumber = 1, SeriesDescription } = displaySet;
return (
<PanelAccordionTrigger
text={`Series #${SeriesNumber} ${SeriesDescription}`}
count={group.items.length}
isActive={isSelected}
group={group}
menu={menu}
marginLeft="0"
/>
);
}
export function SeriesMeasurements(props): React.ReactNode {
const { items, grouping = {}, children } = props;
const system = useSystem();
const activeDisplaySets = useActiveViewportDisplaySets(system);
const activeDisplaySetInstanceUID = activeDisplaySets?.[0]?.displaySetInstanceUID;
const onClick = (_e, group) => {
const { items } = group;
system.commandsManager.run('jumpToMeasurement', {
uid: items[0].uid,
displayMeasurements: items,
group,
});
};
// The content of the accordion group will default to the children of the
// parent declaration if present, otherwise to MeasurementItems
return (
<AccordionGroup
grouping={{
groupingFunction: groupByDisplaySet,
activeDisplaySetInstanceUID,
...grouping,
onClick,
}}
items={items}
sourceChildren={children}
>
<AccordionGroup.Trigger asChild={true}>
<SeriesMeasurementTrigger menu={MeasurementsMenu} />
</AccordionGroup.Trigger>
<MeasurementItems />
</AccordionGroup>
);
}
export default SeriesMeasurements;
Loaded 100 of 1701 files, more files were not shown because too many files have changed in this diff. Show more