Compare commits

..
Author SHA1 Message Date
ohif-bot afa9131da1 chore(version): Update package versions [skip ci] 2025-10-27 19:21:49 +00:00
ohif-bot 5b5bca5e91 chore(version): version.json [skip ci] 2025-10-27 19:21:42 +00:00
Joe Boccanfuso 1f11c83006 fix(security): Addressed dependabot alert CVE-2025-59288 concerning playwright (#5527) 2025-10-27 15:16:43 -04:00
ohif-bot 1dd4f60034 chore(version): Update package versions [skip ci] 2025-10-24 20:53:28 +00:00
ohif-bot 2013c87ddb chore(version): version.json [skip ci] 2025-10-24 20:53:21 +00:00
Joe Boccanfuso 1009c60911 fix(security): For bun, enforce frozen lockfile. For yarn, strongly suggest using frozen lockfile. (#5508)
* Updated docs for security information.
2025-10-24 16:48:35 -04:00
ohif-bot b517944cb5 chore(version): Update package versions [skip ci] 2025-10-23 13:17:02 +00:00
ohif-bot 67d6622ae4 chore(version): version.json [skip ci] 2025-10-23 13:16:54 +00:00
Ibrahim a1544432db feat(jpeg2000): Add 16-bit RGB support to JPEG2000 decoder (#5519) 2025-10-23 09:11:54 -04:00
ohif-bot e0e1763472 chore(version): Update package versions [skip ci] 2025-10-22 16:18:31 +00:00
ohif-bot b844b56839 chore(version): version.json [skip ci] 2025-10-22 16:18:25 +00:00
k3motandTom Avitan 8b9445c442 fix: Fixed the tooltip text of the window button, it was mistakenly written in Chinese in the en-us json (#5513)
Fixed the tooltip text of the window button, it was mistakenly written in Chinese in the en-US json

Co-authored-by: Tom Avitan <tom.avitan@aidoc.com>
2025-10-22 12:14:00 -04:00
ohif-bot 8193311841 chore(version): Update package versions [skip ci] 2025-10-22 15:00:24 +00:00
ohif-bot c83710fa78 chore(version): version.json [skip ci] 2025-10-22 15:00:18 +00:00
Joe Boccanfuso e07a87dcae chore(workflow): Run playwright tests for release branch PRs. (#5515) 2025-10-22 10:55:46 -04:00
ohif-bot 2bc0647861 chore(version): Update package versions [skip ci] 2025-10-21 16:03:13 +00:00
ohif-bot 2a01dcce29 chore(version): version.json [skip ci] 2025-10-21 16:03:06 +00:00
Joe Boccanfuso 1df4f843fe fix(segmentation): Fixed the manual scrolling of the segments list and implemented automatic scrolling to the active segment. (#5510)
The height of the list container is now calculated based on a parent div instead of itself.
DataRow component now has a forward ref to allow for automatic scrolling.

Added better detection to the useDynamicMaxHeight hook for when to recalculate the max height using intersection observers.

The useEffect for scrolling a segment into view used to be excessively called to the point
where it was auto scrolling after the user manually scrolled. The auto scroll useEffect is
now dependent on the segment index changing which should only happen once per active segment change.
2025-10-21 11:58:23 -04:00
ohif-bot cf0b561a49 chore(version): Update package versions [skip ci] 2025-10-20 17:12:13 +00:00
ohif-bot f1675559ee chore(version): version.json [skip ci] 2025-10-20 17:12:07 +00:00
Ibrahim ffd9ec3273 fix(rendering): Fix palette color LUT conversion causing black images (#5509)
The palette color LUT was incorrectly handling 16 bit entries, and didn't work for both array buffers and typed arrays.
2025-10-20 13:07:39 -04:00
ohif-bot 66e6d73ad6 chore(version): Update package versions [skip ci] 2025-10-20 15:28:17 +00:00
ohif-bot 07c45d0364 chore(version): version.json [skip ci] 2025-10-20 15:28:10 +00:00
Bill Wallace 503780247c fix: Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data (#5506)
* fix: [#2404] by passing the right sop uid map

* Update PR with requried CS3D fixes

* Update to released versions - had wrong release number
2025-10-20 11:23:26 -04:00
ohif-bot 82c4a0a8d0 chore(version): Update package versions [skip ci] 2025-10-18 02:18:57 +00:00
ohif-bot 6a523ff78d chore(version): version.json [skip ci] 2025-10-18 02:18:51 +00:00
Joe Boccanfuso 170e860aa1 fix(segmentation): Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. (#5503)
* fix(segmentation): Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true.

* PR feedback.

* PR feedback.

* Added tests for disableEditing and utilities for simulating drag operations.

* Update bun.lock file after merging master.
2025-10-17 22:14:31 -04:00
ohif-bot 3e1d50c8d2 chore(version): Update package versions [skip ci] 2025-10-17 12:19:47 +00:00
ohif-bot 05b80305dc chore(version): version.json [skip ci] 2025-10-17 12:19:40 +00:00
Joe Boccanfuso 4620cc3acf fix(ErrorBoundary): Allow for details to be shown in production. (#5504)
Added showErrorDetails to config to allow for details to be shown in various runtime environments.
2025-10-17 08:15:13 -04:00
ohif-bot cf8f4aa120 chore(version): Update package versions [skip ci] 2025-10-17 01:59:53 +00:00
ohif-bot f75273ece6 chore(version): version.json [skip ci] 2025-10-17 01:59:47 +00:00
Luis Miguel SantosandBill Wallace 42aa2df626 fix 5323 unexpected series list filter change from all to recent (#5337)
* fix to unexpected change from All to recent browser study lists because of the search order for the presence of the study in the tab groups and subsequent reset of the active tab state.
Minor comment in the study browser header.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore Added type annotations and cleaned up stylistically.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* Tighten up a bit of the code to make it easier to follow

* Fix warnings

---------

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

Confirm that Alireza's requested change was included.
2025-10-16 21:55:31 -04:00
ohif-bot be6f3db1ce chore(version): Update package versions [skip ci] 2025-10-17 00:07:14 +00:00
ohif-bot c41dbc8236 chore(version): version.json [skip ci] 2025-10-17 00:07:08 +00:00
53694da216 [Ready] fix 5288 removal of accept header options in retrieve metadata (#5293)
* chore: Minor adjustment to generateAcceptHeader function signature to reflect expected return type.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* fix: Added switch to skip the Accept header generation when requesting metadata. WADO metadata request is more likely to return JSON and some VNAs do not like the extra options in the Accept header. Also, passing an empty array is not sufficient because somewhere we still include a comma that breaks the Accept header. It's better to omit it for the metadata request only.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Added new HeadersInterface interface type docstrings.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Refactored getAuthorizationHeader() function signature so it is typed checked. Of course, I upgraded the module to a TypeScript module.
Moved the request header interfaces into its own TypeScript module (RequestHeaders.ts) in the core types.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Refactored the user logic to include a TypeScript interface. As a result, upgraded the source file to TypeScript.
Removed the User interface from RequestHeaders module and moved them to the user module.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Updated function signatures, user import, and confirmed unit tests are passing.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Minor stylistic adjustment to getAuthorizationHeader.test.ts

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore: Added missing comments.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* chore adjusted generateWadoHeader parameter's name and added comment about the expected default header.

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>

* Just tweaking the interface a bit to be more consistent.

* fix authorization header signature change

---------

Signed-off-by: Luis M. Santos <luis.santos2@nih.gov>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2025-10-16 20:01:34 -04:00
ohif-bot 9d68b6ef31 chore(version): Update package versions [skip ci] 2025-10-15 14:51:20 +00:00
ohif-bot 2300c70314 chore(version): version.json [skip ci] 2025-10-15 14:51:13 +00:00
Joe Boccanfuso 86e924fb88 fix(docker): Use bun version 1.2.23 for docker build. (#5501) 2025-10-15 10:46:47 -04:00
ohif-bot e988216b2b chore(version): Update package versions [skip ci] 2025-10-15 13:40:22 +00:00
ohif-bot 23c8fdca00 chore(version): version.json [skip ci] 2025-10-15 13:40:14 +00:00
Devu-trenser faea09e7ef fix: Segmentation not displayed and unable to draw segments in volume viewport after converting Labelmap to Surface (#5488)
* fix: unable to draw segment after 3d layout

* comment update

---------
2025-10-15 09:35:26 -04:00
ohif-bot 05ecdfe512 chore(version): Update package versions [skip ci] 2025-10-15 11:37:08 +00:00
ohif-bot 0b6c38e5e0 chore(version): version.json [skip ci] 2025-10-15 11:37:01 +00:00
Pedro Köhler 3137aed017 fix(segmentationService): prevent no representation crash (#5495) 2025-10-15 07:32:27 -04:00
ohif-bot a1c6b5f3ec chore(version): Update package versions [skip ci] 2025-10-15 04:13:38 +00:00
ohif-bot be13e7506a chore(version): version.json [skip ci] 2025-10-15 04:13:32 +00:00
Joe Boccanfuso 0d2f4e715a fix(docs): Set exact version for glob. (#5497) 2025-10-15 00:08:57 -04:00
ohif-bot 37231a5ffe chore(version): Update package versions [skip ci] 2025-10-15 03:11:55 +00:00
ohif-bot c53ee335ee chore(version): version.json [skip ci] 2025-10-15 03:11:48 +00:00
Joe Boccanfuso fbd9371884 fix(security): Run bun audit before bun install. (#5496) 2025-10-14 23:07:00 -04:00
ohif-bot 5d57c05217 chore(version): Update package versions [skip ci] 2025-10-14 20:28:31 +00:00
ohif-bot ec38af8c06 chore(version): version.json [skip ci] 2025-10-14 20:28:24 +00:00
Joe Boccanfuso c7d2017f08 fix(security): Use exact versioning for dependencies in package.json files. (#5494)
Added bun audit security check in build process.
Removed bun caching.
2025-10-14 16:23:36 -04:00
ohif-bot 3d393df45d chore(version): Update package versions [skip ci] 2025-10-14 17:10:35 +00:00
ohif-bot 2f94e27d3b chore(version): version.json [skip ci] 2025-10-14 17:10:28 +00:00
Ujjwal Kumar Baranwal e9b57d5731 Update Dockerfile (#5486) 2025-10-14 13:06:01 -04:00
ohif-bot ff4ffbe975 chore(version): Update package versions [skip ci] 2025-10-14 17:00:02 +00:00
ohif-bot 458311252b chore(version): version.json [skip ci] 2025-10-14 16:59:55 +00:00
Joe Boccanfuso 3fe67d5c98 fix(bun): Use fixed bun version (#5493) 2025-10-14 12:55:32 -04:00
ohif-bot 87f78069e7 chore(version): Update package versions [skip ci] 2025-10-10 13:19:03 +00:00
ohif-bot f8f7a175c4 chore(version): version.json [skip ci] 2025-10-10 13:18:56 +00:00
Ujjwal Kumar Baranwal 41c028781b fix: Update Dockerfile (#5484)
* Update Dockerfile

* Update Dockerfile
2025-10-10 09:14:11 -04:00
ohif-bot f6cf4d127d chore(version): Update package versions [skip ci] 2025-10-09 20:56:01 +00:00
ohif-bot 131fb57cc9 chore(version): version.json [skip ci] 2025-10-09 20:55:54 +00:00
Joe Boccanfuso efe97cd01d chore(deps): Update cornerstone3D dependencies. (#5479) 2025-10-09 16:51:34 -04:00
ohif-bot 17e1641226 chore(version): Update package versions [skip ci] 2025-10-08 14:37:01 +00:00
ohif-bot be1b44a4ba chore(version): version.json [skip ci] 2025-10-08 14:36:52 +00:00
Joe Boccanfuso 84f27593a0 chore(deps): Update Cornerstone 3D to 4.5.3. (#5473) 2025-10-08 10:30:27 -04:00
ohif-bot 2af95988df chore(version): Update package versions [skip ci] 2025-10-07 23:24:02 +00:00
ohif-bot e2901eb30f chore(version): version.json [skip ci] 2025-10-07 23:23:54 +00:00
Joe Boccanfuso 377d5e2fb6 chore(deps): Update cornerstone dependencies. (#5470) 2025-10-07 19:18:10 -04:00
ohif-bot d3adeec109 chore(version): Update package versions [skip ci] 2025-10-07 21:45:41 +00:00
ohif-bot fc3aec7a79 chore(version): version.json [skip ci] 2025-10-07 21:45:34 +00:00
Devu-trenserandVinicius Faria 78df8ff03a fix(segmentation): Segmentation not displayed and unable to draw segments when switching back to volume/stack viewport from 3D viewport (#5430)
Co-authored-by: Vinicius Faria <viniciusfariaresende@gmail.com>
2025-10-07 18:40:32 -03:00
ohif-bot c4902f2b9e chore(version): Update package versions [skip ci] 2025-10-07 18:35:26 +00:00
ohif-bot 1423ecc9d8 chore(version): version.json [skip ci] 2025-10-07 18:35:16 +00:00
devin-ai-integration[bot]andJoe Boccanfuso 5040c947d2 fix: validation for Percentage of Max SUV input field in TMTV module (#5417)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2025-10-07 14:29:07 -04:00
ohif-bot c05698c5ec chore(version): Update package versions [skip ci] 2025-10-07 17:29:12 +00:00
ohif-bot 3dd18a453a chore(version): version.json [skip ci] 2025-10-07 17:29:04 +00:00
Vinícius Alves de Faria Resende 2c6ce21c8f fix(Segmentation): [Bug #5420] Segmentation color resets after using toggleOneUp (#5465) 2025-10-07 13:23:26 -04:00
ohif-bot 0803dce328 chore(version): Update package versions [skip ci] 2025-10-07 15:48:25 +00:00
ohif-bot 990ae853b0 chore(version): version.json [skip ci] 2025-10-07 15:48:17 +00:00
Vinícius Alves de Faria Resende 3c421f124e test(CornerstoneUtils): Adding unit tests for most of Cornerstone Extension utilities (#5454) 2025-10-07 11:42:47 -04:00
ohif-bot 44beac799f chore(version): Update package versions [skip ci] 2025-10-07 13:15:00 +00:00
ohif-bot 3b4b59c32b chore(version): version.json [skip ci] 2025-10-07 13:14:53 +00:00
Pedro Köhler 7f12f398fb fix(measurementTrackingMachine): remove unreachable states (#5460) 2025-10-07 09:09:51 -04:00
ohif-bot 891b752e69 chore(version): Update package versions [skip ci] 2025-10-07 12:59:00 +00:00
ohif-bot 42bf9c1484 chore(version): version.json [skip ci] 2025-10-07 12:58:52 +00:00
Pedro Köhler 4d04dd5651 fix(toolbar): prevent duplicate command execution on repeated interactions (#5456) 2025-10-07 08:52:56 -04:00
ohif-bot 9e79e76bab chore(version): Update package versions [skip ci] 2025-10-06 13:44:50 +00:00
ohif-bot a58371e83b chore(version): version.json [skip ci] 2025-10-06 13:44:43 +00:00
d2ae13b03f fix: viewport ready event firing logic (#5462)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 09:39:56 -04:00
ohif-bot c9b687900e chore(version): Update package versions [skip ci] 2025-10-06 13:35:37 +00:00
ohif-bot 282633f907 chore(version): version.json [skip ci] 2025-10-06 13:35:30 +00:00
Pedro Köhler e2964bf669 fix(router): opt in to React Router v7 future flags (#5461) 2025-10-06 09:30:36 -04:00
ohif-bot fae3b25b7b chore(version): Update package versions [skip ci] 2025-10-06 13:01:53 +00:00
ohif-bot 64ee6d529f chore(version): version.json [skip ci] 2025-10-06 13:01:45 +00:00
Sofien-Sellami 792db26ff1 fix: Safely handle missing active viewport (#5448) 2025-10-06 08:56:31 -04:00
ohif-bot 60633a8f3f chore(version): Update package versions [skip ci] 2025-10-06 12:56:17 +00:00
ohif-bot 07eeb8e5fe chore(version): version.json [skip ci] 2025-10-06 12:56:08 +00:00
Igor Octaviano 851e74d7b8 feat(MeasurementService): add rendering of unmapped measurements (#5416) 2025-10-06 08:50:17 -04:00
ohif-bot 639863a8ef chore(version): Update package versions [skip ci] 2025-10-03 20:36:59 +00:00
ohif-bot a00f610b2d chore(version): version.json [skip ci] 2025-10-03 20:36:52 +00:00
AlirezaandCursor Agent 9e951e8800 fix: duplicate segmentation entries on layout switch (#5458)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-10-03 16:31:25 -04:00
ohif-bot 71c16615d1 chore(version): Update package versions [skip ci] 2025-10-03 16:32:53 +00:00
ohif-bot b1fc82315c chore(version): version.json [skip ci] 2025-10-03 16:32:46 +00:00
AlirezaandCursor Agent 6a1df9de4a fix: study browser mode for multiple studies (#5457)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-10-03 12:28:05 -04:00
ohif-bot 06445dcaa2 chore(version): Update package versions [skip ci] 2025-10-02 21:10:24 +00:00
ohif-bot a344183355 chore(version): version.json [skip ci] 2025-10-02 21:10:17 +00:00
ZZzzaaKK 2f28a0deab fix(studyBrowserCustomization): fix thumbnail menu item actions in study browser (#5283) 2025-10-02 17:05:45 -04:00
ohif-bot e04310a02d chore(version): Update package versions [skip ci] 2025-10-01 18:37:20 +00:00
ohif-bot 4efb7efd0e chore(version): version.json [skip ci] 2025-10-01 18:37:11 +00:00
Bill WallaceandIbrahim 9ad0d7fc8c feat: Add inherit from basic/longitudinal modes (#5435)
Co-authored-by: Ibrahim <ibrahim.mdev@gmail.com>
2025-10-01 14:31:26 -04:00
ohif-bot 71d21ee184 chore(version): Update package versions [skip ci] 2025-09-30 22:34:04 +00:00
ohif-bot 9677dfdb90 chore(version): version.json [skip ci] 2025-09-30 22:33:57 +00:00
Smoothengineer 5b361fb289 docs: grammar: change "it's" to "its" (#5451) 2025-09-30 18:29:00 -04:00
ohif-bot d9c5b840a5 chore(version): Update package versions [skip ci] 2025-09-30 19:12:30 +00:00
ohif-bot ec3a70b5a7 chore(version): version.json [skip ci] 2025-09-30 19:12:18 +00:00
Joe Boccanfuso a46b00f76b chore(deps): Update cornerstone3D to 4.3.14. (#5450) 2025-09-30 15:07:13 -04:00
ohif-bot f0e9865ca1 chore(version): Update package versions [skip ci] 2025-09-30 17:55:16 +00:00
ohif-bot 0d37705435 chore(version): version.json [skip ci] 2025-09-30 17:55:07 +00:00
Vinícius Alves de Faria Resende 12ac509c40 test(SegmentationService): Adding 98.63% of test line coverage to Segmentation Service through unit tests (#5421) 2025-09-30 13:49:13 -04:00
ohif-bot 9353a840ee chore(version): Update package versions [skip ci] 2025-09-29 20:01:26 +00:00
ohif-bot 8030577fb2 chore(version): version.json [skip ci] 2025-09-29 20:01:18 +00:00
Joe Boccanfuso dd86a8eabd fix(segmentation): Restored segment label tool in longitudinal mode. (#5443) 2025-09-29 15:56:11 -04:00
ohif-bot 47164463cc chore(version): Update package versions [skip ci] 2025-09-29 16:53:02 +00:00
ohif-bot 00b74664f9 chore(version): version.json [skip ci] 2025-09-29 16:52:54 +00:00
Dan RukasandJoe Boccanfuso c91752d575 ui(settings): Moved Segment Label Display from tools to settings panel (#5422)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2025-09-29 12:47:45 -04:00
ohif-bot 41d4398342 chore(version): Update package versions [skip ci] 2025-09-29 02:32:36 +00:00
ohif-bot f0a5795b68 chore(version): version.json [skip ci] 2025-09-29 02:32:30 +00:00
Joe Boccanfuso 80f314a422 fix(security): Removed dependency on tar-fs by removing dependencies on storybook and sharp. (#5438) 2025-09-28 22:28:06 -04:00
ohif-bot a6408d6033 chore(version): Update package versions [skip ci] 2025-09-26 14:10:20 +00:00
ohif-bot 5332d3e5f5 chore(version): version.json [skip ci] 2025-09-26 14:10:12 +00:00
Joe Boccanfuso d41eca4ed5 chore(deps): update cornerstone to 4.3.10 (#5436) 2025-09-26 10:04:30 -04:00
ohif-bot a5ddcf43de chore(version): Update package versions [skip ci] 2025-09-25 18:12:56 +00:00
ohif-bot bd19ab983d chore(version): version.json [skip ci] 2025-09-25 18:12:49 +00:00
Igor Octaviano d88a341884 fix: Improve rt handling for bulkdata (#5415) 2025-09-25 14:07:35 -04:00
ohif-bot 6db50d26b2 chore(version): Update package versions [skip ci] 2025-09-25 14:26:37 +00:00
ohif-bot 3906c754f1 chore(version): version.json [skip ci] 2025-09-25 14:26:30 +00:00
毛知劲 | LIS/PACS/RIS 产品负责人 7783d0f558 fix(i18n): Add and update i18n Translation for Toolbar Tool Names (#5392) 2025-09-25 10:22:02 -04:00
ohif-bot add69e5e4f chore(version): Update package versions [skip ci] 2025-09-22 02:53:26 +00:00
ohif-bot d32edcca45 chore(version): version.json [skip ci] 2025-09-22 02:53:19 +00:00
Vinícius Alves de Faria Resende 69dbe27784 feat(Segmentation): Segmentation highlight animation function selection (#5401) 2025-09-21 22:48:35 -04:00
ohif-bot de201d567f chore(version): Update package versions [skip ci] 2025-09-19 13:30:27 +00:00
ohif-bot 26a042d8b9 chore(version): version.json [skip ci] 2025-09-19 13:30:20 +00:00
Vinícius Alves de Faria Resende 74b82bd934 fix(SegmentationService): _getSegmentCenter wrongly handling seg center availability (#5396) 2025-09-19 09:25:32 -04:00
ohif-bot a7576e07a7 chore(version): Update package versions [skip ci] 2025-09-17 19:40:05 +00:00
ohif-bot 66b2f16c9e chore(version): version.json [skip ci] 2025-09-17 19:39:58 +00:00
Joe Boccanfuso cf10e3cf67 chore(tests): update screenshots (#5413) 2025-09-17 15:35:13 -04:00
ohif-bot 3ea981b40c chore(version): Update package versions [skip ci] 2025-09-17 16:07:53 +00:00
ohif-bot e1ce9c63ee chore(version): version.json [skip ci] 2025-09-17 16:07:46 +00:00
dependabot[bot]andJoe Boccanfuso 06073af035 chore(deps): bump axios from 1.11.0 to 1.12.0 (#5412)
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-09-17 12:02:33 -04:00
ohif-bot 19f7bba5d8 chore(version): Update package versions [skip ci] 2025-09-16 19:56:57 +00:00
ohif-bot 5f7045eb09 chore(version): version.json [skip ci] 2025-09-16 19:56:51 +00:00
dependabot[bot]andJoe Boccanfuso 8610c862a9 chore(deps): bump axios from 1.11.0 to 1.12.0 (#5402)
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-09-16 15:52:13 -04:00
ohif-bot 8c8a4c2222 chore(version): Update package versions [skip ci] 2025-09-10 16:44:52 +00:00
ohif-bot 57cc0b0ae3 chore(version): version.json [skip ci] 2025-09-10 16:44:45 +00:00
Vinícius Alves de Faria Resende 56c5828b02 fix(ErrorBoundary): Fixing console errors and Improving error logging for better developer experience (#5378) 2025-09-10 12:40:09 -04:00
ohif-bot f6d05bc21c chore(version): Update package versions [skip ci] 2025-09-05 12:39:54 +00:00
ohif-bot a5d72a130a chore(version): version.json [skip ci] 2025-09-05 12:39:47 +00:00
Vinícius Alves de Faria Resende d6ea179ac4 fix(cornerstone-dicom-rt): adding caching usage to cornerstone-dicom-rt SOP Class Handler (#5387) 2025-09-05 08:35:16 -04:00
ohif-bot 7de1c78fc4 chore(version): Update package versions [skip ci] 2025-08-29 13:16:02 +00:00
ohif-bot 5f8f559426 chore(version): version.json [skip ci] 2025-08-29 13:15:55 +00:00
Omar Hamad 79473c2ea4 fix(oidc): resolve navigation throttling and blank screen during sign-in/sign-out with Keycloak + dcm4chee + Traefik (#5373) 2025-08-29 09:11:09 -04:00
ohif-bot ecf91bdcd4 chore(version): Update package versions [skip ci] 2025-08-29 12:22:34 +00:00
ohif-bot 2df818c8cd chore(version): version.json [skip ci] 2025-08-29 12:22:27 +00:00
이양구andBill Wallace 0f6f2bb432 fix: routerBasename declaration location (#5347)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-08-29 08:17:38 -04:00
ohif-bot 9f2835be2d chore(version): Update package versions [skip ci] 2025-08-28 19:12:46 +00:00
ohif-bot 01a1265291 chore(version): version.json [skip ci] 2025-08-28 19:12:40 +00:00
Copilotandsedghi 83ee20d249 Replace individual system info fields with envinfo command in bug report template (#5368)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sedghi <7490180+sedghi@users.noreply.github.com>
2025-08-28 15:08:02 -04:00
ohif-bot a0a571cb4b chore(version): Update package versions [skip ci] 2025-08-28 18:04:29 +00:00
ohif-bot d45fd931b3 chore(version): version.json [skip ci] 2025-08-28 18:04:23 +00:00
Alireza fc0e7f8572 upgrade cs3d to 4.0 (#5371) 2025-08-28 13:59:26 -04:00
ohif-bot cb9c099f36 chore(version): Update package versions [skip ci] 2025-08-28 17:02:31 +00:00
ohif-bot ccb8a0dcc3 chore(version): version.json [skip ci] 2025-08-28 17:02:24 +00:00
Alireza 6a1838bf0d fix/migration 3p11 (#5370) 2025-08-28 12:57:45 -04:00
ohif-bot df0593aac9 chore(version): Update package versions [skip ci] 2025-08-28 16:15:59 +00:00
ohif-bot aa9b881c54 chore(version): version.json [skip ci] 2025-08-28 16:15:51 +00:00
Pedro Köhler d91cc230e5 fix: seg preview (#5338) 2025-08-28 12:11:03 -04:00
ohif-bot e0f7b5b5db chore(version): Update package versions [skip ci] 2025-08-25 16:13:39 +00:00
ohif-bot ada7fb45e1 chore(version): version.json [skip ci] 2025-08-25 16:13:32 +00:00
Alireza 368a176c09 fix: docker deploy (#5359) 2025-08-25 12:08:07 -04:00
ohif-bot 25d411ecf9 chore(version): Update package versions [skip ci] 2025-08-25 15:19:11 +00:00
ohif-bot 6f839142de chore(version): version.json [skip ci] 2025-08-25 15:19:04 +00:00
Copilotandsedghi bedb1854f9 fix: Docker build failure due to missing lerna command (#5357)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sedghi <7490180+sedghi@users.noreply.github.com>
2025-08-25 11:14:30 -04:00
ohif-bot 7f8cc666a5 chore(version): Update package versions [skip ci] 2025-08-25 14:27:59 +00:00
ohif-bot 132db911c6 chore(version): version.json [skip ci] 2025-08-25 14:27:52 +00:00
d2694048a4 fix: local ModalitiesInStudy check (#5285)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2025-08-25 10:22:40 -04:00
ohif-bot 722efe9587 chore(version): Update package versions [skip ci] 2025-08-25 14:17:15 +00:00
ohif-bot 7e8f702eb3 chore(version): version.json [skip ci] 2025-08-25 14:17:08 +00:00
Pedro Köhler 21d6ba2bb3 fix: add back clear segmentation representations (#5336) 2025-08-25 10:12:33 -04:00
ohif-bot 9b39719149 chore(version): Update package versions [skip ci] 2025-08-25 14:01:15 +00:00
ohif-bot 2f66c7746c chore(version): version.json [skip ci] 2025-08-25 14:01:09 +00:00
Salim Kanoun b9d6f558cb fix: Segmentation stats customization module (#5324) 2025-08-25 09:56:36 -04:00
ohif-bot e071ed740f chore(version): Update package versions [skip ci] 2025-08-25 13:11:45 +00:00
ohif-bot 0b4020a7e0 chore(version): version.json [skip ci] 2025-08-25 13:11:38 +00:00
Igor Octaviano 840b5d37d1 fix: Add seriesInstanceUID fallback for callByRetrieveAETitle (#5332) 2025-08-25 09:06:52 -04:00
ohif-bot 49dddc50d3 chore(version): Update package versions [skip ci] 2025-08-22 14:35:20 +00:00
ohif-bot 396f75b906 chore(version): version.json [skip ci] 2025-08-22 14:35:13 +00:00
6fe9df6ec9 feat: Jump to measurement moves the camera if it is not in the viewport (#5023)
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
2025-08-22 10:30:18 -04:00
ohif-bot 16759506e5 chore(version): Update package versions [skip ci] 2025-08-22 13:41:26 +00:00
ohif-bot 0688fac419 chore(version): version.json [skip ci] 2025-08-22 13:41:19 +00:00
dependabot[bot] e1112b7abe chore(deps): bump sha.js from 2.4.11 to 2.4.12 (#5350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 09:36:19 -04:00
Pedro Köhler a2133ef6cb fix: header icon logic (#5339) 2025-08-22 09:34:41 -04:00
ohif-bot 824816154b chore(version): Update package versions [skip ci] 2025-08-20 12:18:35 +00:00
ohif-bot 2fe9866b28 chore(version): version.json [skip ci] 2025-08-20 12:18:28 +00:00
Igor OctavianoandBill Wallace 2ed6818171 feat: Add WSI bulk data annotations support (ANN) (#4972)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2025-08-20 08:13:30 -04:00
ohif-bot 0164ad5519 chore(version): Update package versions [skip ci] 2025-08-15 18:17:11 +00:00
ohif-bot 244a91fda5 chore(version): version.json [skip ci] 2025-08-15 18:17:04 +00:00
Bill Wallace becba7861f fix: load 3dsr (#5335) 2025-08-15 14:12:34 -04:00
ohif-bot 730de56a5f chore(version): Update package versions [skip ci] 2025-08-13 20:09:38 +00:00
ohif-bot 72372c022f chore(version): version.json [skip ci] 2025-08-13 20:09:30 +00:00
Leonardo Campos 266b1f58f0 feat(orientationMarkers): improved the orientation markers with a simplified version (#5328) 2025-08-13 16:04:50 -04:00
ohif-bot ede7c4e3ee chore(version): Update package versions [skip ci] 2025-08-11 18:10:59 +00:00
ohif-bot a055cfe924 chore(version): version.json [skip ci] 2025-08-11 18:10:51 +00:00
Ujjwal Kumar Baranwal 0e83f4515c fix: Update Dockerfile (#5319) 2025-08-11 14:06:07 -04:00
ohif-bot 51b2303b0a chore(version): Update package versions [skip ci] 2025-08-11 14:41:28 +00:00
ohif-bot 078cac76bf chore(version): version.json [skip ci] 2025-08-11 14:41:21 +00:00
Alireza 07b310d6e6 docs: fix nav bar (#5322) 2025-08-11 10:36:28 -04:00
ohif-bot f36eeb182c chore(version): Update package versions [skip ci] 2025-08-11 13:53:50 +00:00
ohif-bot 6242f28bc5 chore(version): version.json [skip ci] 2025-08-11 13:53:43 +00:00
Alireza 2553b54432 docs: update release banner (#5321) 2025-08-11 09:48:32 -04:00
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
1851 changed files with 119359 additions and 38649 deletions

No files matched your search

+38 -16
View File
@@ -15,22 +15,13 @@ defaults: &defaults
commands:
install_bun:
steps:
- restore_cache:
keys:
- bun-cache-v2-{{ arch }}-latest
- run:
name: Install Bun
command: |
if [ ! -d "$HOME/.bun" ]; then
curl -fsSL https://bun.sh/install | bash
fi
curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.23"
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV
source $BASH_ENV
- save_cache:
key: bun-cache-v2-{{ arch }}-latest
paths:
- ~/.bun
jobs:
UNIT_TESTS:
@@ -121,6 +112,34 @@ jobs:
- checkout
- attach_workspace:
at: ~/repo
# SECURITY AUDIT
- run:
name: 'Security Audit - High Risk Vulnerabilities'
command: |
echo "🔍 Running bun audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..."
if bun audit --audit-level high; then
echo "✅ No high-risk vulnerabilities found"
echo "🎉 Security audit passed!"
else
echo ""
echo "❌ HIGH-RISK VULNERABILITIES DETECTED!"
echo "======================================"
echo ""
echo "🔧 To fix these issues:"
echo " 1. Run: bun audit"
echo " 2. Review the vulnerability details"
echo " 3. Update affected packages to secure versions"
echo " 4. Test your changes"
echo " 5. Re-run: bun audit --audit-level high"
echo ""
echo "📋 Full audit report:"
bun audit --audit-level low || true
echo ""
echo "❌ This build cannot proceed until high-risk vulnerabilities are resolved."
exit 1
fi
- run:
name: Install Dependencies
command: bun install --frozen-lockfile
@@ -242,7 +261,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 +358,6 @@ jobs:
resource_class: large
parallelism: 8
steps:
- install_bun
- run:
name: Install System Dependencies
command: |
@@ -353,12 +371,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 --frozen-lockfile --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 +386,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 +474,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'
+22 -21
View File
@@ -51,28 +51,29 @@ body:
validations:
required: true
- type: input
id: os
- type: textarea
id: system_info
attributes:
label: 'OS'
description: 'Your operating system.'
placeholder: 'e.g., Windows 10, macOS 10.15.4'
validations:
required: true
- type: input
id: node-version
attributes:
label: 'Node version'
description: 'Your Node.js version.'
placeholder: 'e.g., 20.18.1'
validations:
required: true
- type: input
id: browser
attributes:
label: 'Browser'
description: 'Your browser.'
placeholder: 'e.g., Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1'
label: 'System Information'
description: 'Please run the following command in your terminal and paste the output:'
placeholder: |
Run: npx envinfo --system --binaries --browsers
Then paste the output here. It should look something like:
System:
OS: Windows 10 10.0.19042
CPU: (8) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Memory: 15.89 GB / 31.74 GB
Shell: 1.0.0 - C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe
Binaries:
Node: 20.18.1 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.22 - C:\Users\user\AppData\Roaming\npm\yarn.CMD
npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
Browsers:
Chrome: 83.0.4103.116
Edge: Spartan (44.19041.1266.0), Chromium (83.0.478.58)
Firefox: 77.0.1
validations:
required: true
+110
View File
@@ -0,0 +1,110 @@
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
with:
bun-version: 1.2.23
- 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}}"
+34 -54
View File
@@ -1,76 +1,56 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
branches: [master, release/*]
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
with:
bun-version: 1.2.23
- 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
*~
+1 -1
View File
@@ -19,7 +19,7 @@ echo 'Web application built and copied'
# Build && Move Docusaurus Output (for the docs themselves)
cd platform/docs
yarn install
yarn install --frozen-lockfile
yarn run build
cd ../..
mkdir -p ./.netlify/www/docs
+1 -1
View File
@@ -10,6 +10,6 @@
"deploy": "netlify deploy --prod --dir ./../platform/app/dist"
},
"devDependencies": {
"netlify-cli": "^2.21.0"
"netlify-cli": "2.21.0"
}
}
+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 -10
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?$/,
@@ -183,8 +201,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
'dicom-microscopy-viewer':
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
},
// Which directories to search when resolving modules
modules: [
+2235
View File
File diff suppressed because it is too large. Load diff
+11 -9
View File
@@ -49,8 +49,8 @@ RUN apt-get update && apt-get install -y build-essential python3
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
RUN npm install -g bun
# RUN npm install -g lerna@7.4.2
RUN npm install -g bun@1.2.23
RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
# Do an initial install and then a final install
@@ -60,11 +60,9 @@ COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/pa
RUN bun pm cache rm
RUN bun install
RUN bun add ajv@8.12.0
# 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 +70,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,20 +83,23 @@ 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
RUN chown -R nginx:nginx /usr/share/nginx/html
RUN chown -R nginx:nginx /usr/share/nginx/html && chmod -R 777 /usr/share/nginx/html
USER nginx
ENTRYPOINT ["/usr/src/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+10 -4
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
@@ -77,7 +76,7 @@ contributions of individuals, research groups, and commercial organizations.
After more than 8-years of integrating with many companies and organizations,
The OHIF Viewer has been rebuilt from the ground up to better address the
varying workflow and configuration needs of its many users. All of the Viewer's
core features are built using it's own extension system. The same extensibility
core features are built using its own extension system. The same extensibility
that allows us to offer:
- 2D and 3D medical image viewing
@@ -148,7 +147,14 @@ Here is a schematic representation of our development workflow:
3. Navigate to the cloned project's directory
4. Add this repo as a `remote` named `upstream`
- `git remote add upstream https://github.com/OHIF/Viewers.git`
5. `yarn install` to restore dependencies and link projects
5. `yarn install --frozen-lockfile` to restore dependencies and link projects
:::danger
In general run `yarn install` with the `--frozen-lockfile` flag to help avoid
supply chain attacks by enforcing reproducible dependencies. That is, if the
`yarn.lock` file is clean and does NOT reference compromised packages, then
no compromised packages should land on your machine by using this flag.
:::
#### To Develop
@@ -159,7 +165,7 @@ _From this repository's root directory:_
yarn config set workspaces-experimental true
# Restore dependencies
yarn install
yarn install --frozen-lockfile
```
## Commands
File diff suppressed because it is too large. Load diff
+73 -76
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"license": "MIT",
"private": true,
"engines": {
@@ -9,88 +9,85 @@
"yarn": ">=1.19.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@babel/runtime": "7.28.2",
"@kitware/vtk.js": "32.12.0",
"clsx": "^2.1.1",
"core-js": "^3.2.1",
"moment": "^2.9.4"
"clsx": "2.1.1",
"core-js": "3.45.1",
"moment": "2.30.1"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@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",
"@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",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^5.2.0",
"css-loader": "^6.8.1",
"dotenv": "^8.1.0",
"eslint": "^8.39.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-promise": "^5.2.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-tsdoc": "^0.2.11",
"eslint-webpack-plugin": "^2.5.3",
"execa": "^8.0.1",
"extract-css-chunks-webpack-plugin": "^4.5.4",
"html-webpack-plugin": "^5.3.2",
"husky": "^3.0.0",
"jest": "^29.5.0",
"jest-canvas-mock": "^2.1.0",
"jest-environment-jsdom": "^29.5.0",
"jest-junit": "^6.4.0",
"lerna": "^7.2.0",
"lint-staged": "^9.0.2",
"mini-css-extract-plugin": "^2.1.0",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"postcss": "^8.3.5",
"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",
"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",
"@babel/eslint-parser": "7.28.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
"@rsbuild/core": "1.5.1",
"@rsbuild/plugin-node-polyfill": "1.4.2",
"@rsbuild/plugin-react": "1.4.0",
"@svgr/webpack": "8.1.0",
"@swc/helpers": "0.5.17",
"@types/jest": "27.5.2",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.21",
"babel-loader": "8.4.1",
"babel-plugin-module-resolver": "5.0.2",
"clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "9.1.0",
"cross-env": "7.0.3",
"css-loader": "6.11.0",
"dotenv": "8.6.0",
"eslint": "8.57.1",
"eslint-config-prettier": "7.2.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-cypress": "2.15.2",
"eslint-plugin-flowtype": "7.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-promise": "5.2.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-tsdoc": "0.2.17",
"eslint-webpack-plugin": "2.7.0",
"execa": "8.0.1",
"extract-css-chunks-webpack-plugin": "4.10.0",
"html-webpack-plugin": "5.6.3",
"husky": "3.1.0",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-environment-jsdom": "29.7.0",
"jest-junit": "6.4.0",
"lerna": "7.4.2",
"lint-staged": "9.5.0",
"mini-css-extract-plugin": "2.9.2",
"optimize-css-assets-webpack-plugin": "6.0.1",
"postcss": "8.5.6",
"postcss-import": "14.1.0",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.8.3",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9",
"react-refresh": "0.14.2",
"semver": "7.7.2",
"serve": "14.2.4",
"shader-loader": "1.3.1",
"shx": "0.3.4",
"source-map-loader": "4.0.2",
"style-loader": "1.3.0",
"terser-webpack-plugin": "5.3.14",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "4.7.3",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.2",
"webpack-hot-middleware": "2.26.1",
"webpack-merge": "5.10.0",
"workbox-webpack-plugin": "6.6.1",
"worker-loader": "3.0.8"
},
"scripts": {
"build": "Included as direct dependency"
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.46.1"
"dicom-microscopy-viewer": "0.48.6"
}
}
+21 -22
View File
@@ -21,31 +21,30 @@
"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/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/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.27.1",
"@babel/plugin-transform-regenerator": "7.28.1",
"@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",
"**/@babel/runtime": "7.28.2",
"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",
"glob-parent": "^6.0.2",
"trim": "^1.0.0",
"package-json": "^8.1.0",
"typescript": "5.5.4",
"sharp": "^0.32.6"
"nth-check": "2.1.1",
"trim-newlines": "5.0.0",
"glob-parent": "6.0.2",
"trim": "1.0.1",
"package-json": "8.1.1",
"typescript": "5.5.4"
}
}
+9 -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,24 @@ 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',
['babel-plugin-transform-import-meta', { module: 'ES6' }],
],
},
production: {
+1598 -2246
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
[install]
frozenLockfile = true
+2
View File
@@ -0,0 +1,2 @@
[install]
frozenLockfile = false
+1 -1
View File
@@ -1 +1 @@
4dd485bd2d927381bbc2910a2ec545c7d4c03868
1f11c83006bff98cb8434f0662b4de55d5b9bf78
+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',
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.136",
"@ohif/extension-cornerstone": "3.10.0-beta.136",
"@ohif/extension-default": "3.10.0-beta.136",
"@ohif/i18n": "3.10.0-beta.136",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.8.1",
"react-router-dom": "^6.8.1"
"@ohif/core": "3.12.0-beta.76",
"@ohif/extension-cornerstone": "3.12.0-beta.76",
"@ohif/extension-default": "3.12.0-beta.76",
"@ohif/i18n": "3.12.0-beta.76",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.8",
"@cornerstonejs/core": "^3.7.8",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.19",
"@cornerstonejs/core": "4.5.19",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
"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',
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,19 +33,19 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.136",
"@ohif/extension-cornerstone": "3.10.0-beta.136",
"@ohif/extension-default": "3.10.0-beta.136",
"@ohif/i18n": "3.10.0-beta.136",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^10.11.0",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"@ohif/core": "3.12.0-beta.76",
"@ohif/extension-cornerstone": "3.12.0-beta.76",
"@ohif/extension-default": "3.12.0-beta.76",
"@ohif/i18n": "3.12.0-beta.76",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"react-color": "^2.19.3"
"@babel/runtime": "7.28.2",
"react-color": "2.19.3"
}
}
@@ -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,9 +1,14 @@
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';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
const { sopClassDictionary } = utils;
const sopClassUids = [sopClassDictionary.RTStructureSetStorage];
const cachedRTStructsSEG = new Set<string>();
const loadPromises = {};
@@ -30,7 +35,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 +58,7 @@ function _getDisplaySetsFromSeries(
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`,
};
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -85,6 +91,7 @@ function _getDisplaySetsFromSeries(
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
unsubscribe();
}
}
@@ -92,45 +99,64 @@ 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 (
(rtDisplaySet.loading || rtDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] &&
_segmentationExistsInCache(rtDisplaySet, segmentationService)
cachedRTStructsSEG.has(rtDisplaySet.displaySetInstanceUID)
) {
return loadPromises[SOPInstanceUID];
}
rtDisplaySet.loading = true;
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
(evt: { rtDisplaySet: { displaySetInstanceUID: string } }) => {
if (evt.rtDisplaySet?.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
cachedRTStructsSEG.add(rtDisplaySet.displaySetInstanceUID);
unsubscribe();
}
}
);
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
loadPromises[SOPInstanceUID] = new Promise<void>(async (resolve, reject) => {
try {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
rtDisplaySet.structureSet = structureSet;
}
rtDisplaySet.structureSet = structureSet;
if (createSegmentation) {
await segmentationService.createSegmentationForRTDisplaySet(rtDisplaySet);
}
resolve();
} catch (error) {
reject(error);
} finally {
rtDisplaySet.loading = false;
}
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
})
.catch(error => {
rtDisplaySet.loading = false;
reject(error);
});
});
return loadPromises[SOPInstanceUID];
@@ -171,20 +197,9 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence;
}
function _segmentationExistsInCache(
rtDisplaySet,
segmentationService: AppTypes.SegmentationService
) {
// Todo: fix this
return false;
// This should be abstracted with the CornerstoneCacheService
const rtContourId = rtDisplaySet.displaySetInstanceUID;
const contour = segmentationService.getContour(rtContourId);
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
return contour !== undefined;
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
return [
{
name: 'dicom-rt',
@@ -2,9 +2,34 @@ import dcmjs from 'dcmjs';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
async function checkAndLoadContourData(instance, datasource) {
/**
* Checks and loads contour data for RT Structure Set, handling both inline and bulk data URIs.
* Processes ROIContourSequence to extract contour data and resolve any bulk data references.
*
* @async
* @function checkAndLoadContourData
* @param {Object} params - Parameters object
* @param {Object} params.instance - Initial RT Structure instance
* @param {Object} params.dataSource - Data source for retrieving bulk data
* @param {Object} params.extensionManager - OHIF extension manager
* @param {Object} params.rtStructDisplaySet - RT Structure display set
* @param {Object} params.headers - HTTP headers for requests
* @returns {Promise<Object>} Promise that resolves to the processed RT Structure instance with loaded contour data
* @throws {Promise<string>} Rejects with error message if instance is invalid or data retrieval fails
*/
async function checkAndLoadContourData({
instance: initialInstance,
dataSource,
extensionManager,
rtStructDisplaySet,
headers,
}) {
let instance = initialInstance;
if (!instance || !instance.ROIContourSequence) {
return Promise.reject('Invalid instance object or ROIContourSequence');
instance = await getRTStructInstance({ extensionManager, rtStructDisplaySet, headers });
if (!instance || !instance.ROIContourSequence) {
return Promise.reject('Invalid instance object or ROIContourSequence');
}
}
const promisesMap = new Map();
@@ -30,11 +55,11 @@ async function checkAndLoadContourData(instance, datasource) {
} else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI;
if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) {
return Promise.reject('Invalid datasource object or retrieve function');
if (!dataSource || !dataSource.retrieve || !dataSource.retrieve.bulkDataURI) {
return Promise.reject('Invalid dataSource object or retrieve function');
}
const bulkDataPromise = datasource.retrieve.bulkDataURI({
const bulkDataPromise = dataSource.retrieve.bulkDataURI({
BulkDataURI: bulkDataURI,
StudyInstanceUID: instance.StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
@@ -87,35 +112,74 @@ async function checkAndLoadContourData(instance, datasource) {
console.error(error);
}
});
return instance;
}
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
/**
* Retrieves and parses RT Structure Set instance from DICOM data.
* Uses the cornerstone utility module to load DICOM data and converts it to a naturalized dataset.
*
* @async
* @function getRTStructInstance
* @param {Object} params - Parameters object
* @param {Object} params.extensionManager - OHIF extension manager
* @param {Object} params.rtStructDisplaySet - RT Structure display set
* @param {Object} params.headers - HTTP headers for requests
* @returns {Promise<Object>} Promise that resolves to the parsed RT Structure dataset
*/
const getRTStructInstance = async ({ extensionManager, rtStructDisplaySet, headers }) => {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
return rtStructDataset;
};
/**
* Main function to load and process RT Structure Set data.
* Creates a structure set object with ROI contours, metadata, and visualization properties.
* Handles both bulk data URI and inline contour data scenarios.
*
* @async
* @function loadRTStruct
* @param {Object} extensionManager - OHIF extension manager
* @param {Object} rtStructDisplaySet - RT Structure display set to process
* @param {Object} headers - HTTP headers for data requests
* @returns {Promise<Object>} Promise that resolves to a structure set object containing:
* - StructureSetLabel: Label of the structure set
* - SeriesInstanceUID: Series instance UID
* - ROIContours: Array of ROI contour data with points and metadata
* - visible: Visibility state
* - ReferencedSOPInstanceUIDsSet: Set of referenced SOP instance UIDs
*/
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
const dataSource = extensionManager.getActiveDataSource()[0];
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
// Set here is loading is asynchronous.
// If this function throws its set back to false.
rtStructDisplaySet.isLoaded = true;
let instance = rtStructDisplaySet.instance;
if (!bulkDataURI || !bulkDataURI.enabled) {
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
instance = rtStructDataset;
instance = await getRTStructInstance({ extensionManager, rtStructDisplaySet, headers });
} else {
await checkAndLoadContourData(instance, dataSource);
instance = await checkAndLoadContourData({
instance,
dataSource,
extensionManager,
rtStructDisplaySet,
headers,
});
}
const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance;
@@ -132,21 +196,16 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
for (let i = 0; i < ROIContourSequence.length; i++) {
const ROIContour = ROIContourSequence[i];
const { ContourSequence } = ROIContour;
if (!ContourSequence) {
continue;
}
const isSupported = false;
const ContourSequenceArray = _toArray(ContourSequence);
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
for (const ContourSequenceItem of ContourSequenceArray) {
const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } =
ContourSequenceArray[c];
let isSupported = false;
ContourSequenceItem;
const points = [];
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
@@ -157,22 +216,18 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
});
}
switch (ContourGeometricType) {
case 'CLOSED_PLANAR':
case 'OPEN_PLANAR':
case 'POINT':
isSupported = true;
break;
default:
continue;
}
const supportedContourTypesMap = new Map([
['CLOSED_PLANAR', false],
['OPEN_NONPLANAR', false],
['OPEN_PLANAR', false],
['POINT', true],
]);
contourPoints.push({
numberOfPoints: NumberOfContourPoints,
points,
type: ContourGeometricType,
isSupported,
isSupported: supportedContourTypesMap.get(ContourGeometricType) ?? false,
});
if (ContourImageSequence?.ReferencedSOPInstanceUID) {
@@ -187,20 +242,30 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour,
contourPoints,
isSupported
contourPoints
);
}
return structureSet;
}
/**
* Sets metadata for ROI contour data and adds it to the structure set.
* Extracts ROI information from StructureSetROISequence and RTROIObservationsSequence,
* then creates a complete ROI contour data object with visualization properties.
*
* @function _setROIContourMetadata
* @param {Object} structureSet - The structure set object to add ROI contour to
* @param {Array} StructureSetROISequence - Array of structure set ROI definitions
* @param {Array} RTROIObservationsSequence - Array of RT ROI observations
* @param {Object} ROIContour - ROI contour object containing contour data
* @param {Array} contourPoints - Array of processed contour points
*/
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour,
contourPoints,
isSupported
contourPoints
) {
const StructureSetROI = StructureSetROISequence.find(
structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
@@ -211,9 +276,9 @@ function _setROIContourMetadata(
ROIName: StructureSetROI.ROIName,
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
ROIDescription: StructureSetROI.ROIDescription,
isSupported,
contourPoints,
visible: true,
colorArray: [],
};
_setROIContourDataColor(ROIContour, ROIContourData);
@@ -230,6 +295,14 @@ function _setROIContourMetadata(
structureSet.ROIContours.push(ROIContourData);
}
/**
* Sets the display color for ROI contour data.
* Uses ROIDisplayColor if available, otherwise converts RecommendedDisplayCIELabValue to RGB.
*
* @function _setROIContourDataColor
* @param {Object} ROIContour - ROI contour object containing color information
* @param {Object} ROIContourData - ROI contour data object to set color on
*/
function _setROIContourDataColor(ROIContour, ROIContourData) {
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
@@ -243,6 +316,15 @@ function _setROIContourDataColor(ROIContour, ROIContourData) {
}
}
/**
* Sets RT ROI observations metadata for ROI contour data.
* Finds matching RTROIObservations by ROINumber and adds observation details to contour data.
*
* @function _setROIContourRTROIObservations
* @param {Object} ROIContourData - ROI contour data object to add observations to
* @param {Array} RTROIObservationsSequence - Array of RT ROI observations
* @param {number} ROINumber - ROI number to match observations
*/
function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) {
const RTROIObservations = RTROIObservationsSequence.find(
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
@@ -262,6 +344,14 @@ function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequen
}
}
/**
* Converts a single object or array to an array.
* Utility function to ensure consistent array handling for DICOM sequences.
*
* @function _toArray
* @param {*} objOrArray - Object or array to convert
* @returns {Array} Array containing the input (if already array) or wrapped in array
*/
function _toArray(objOrArray) {
return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
}
@@ -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',
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.136",
"@ohif/extension-cornerstone": "3.10.0-beta.136",
"@ohif/extension-default": "3.10.0-beta.136",
"@ohif/i18n": "3.10.0-beta.136",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^12.2.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1"
"@ohif/core": "3.12.0-beta.76",
"@ohif/extension-cornerstone": "3.12.0-beta.76",
"@ohif/extension-default": "3.12.0-beta.76",
"@ohif/i18n": "3.12.0-beta.76",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "12.3.1",
"react-router": "6.30.1",
"react-router-dom": "6.30.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.8",
"@cornerstonejs/core": "^3.7.8",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.19",
"@cornerstonejs/core": "4.5.19",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
"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,8 +181,13 @@ 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;
@@ -227,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
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF",
"license": "MIT",
@@ -34,21 +34,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.136",
"@ohif/extension-cornerstone": "3.10.0-beta.136",
"@ohif/extension-measurement-tracking": "3.10.0-beta.136",
"@ohif/ui": "3.10.0-beta.136",
"dcmjs": "*",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1"
"@ohif/core": "3.12.0-beta.76",
"@ohif/extension-cornerstone": "3.12.0-beta.76",
"@ohif/extension-measurement-tracking": "3.12.0-beta.76",
"@ohif/ui": "3.12.0-beta.76",
"dcmjs": "0.43.1",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
"react": "18.3.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.8",
"@cornerstonejs/core": "^3.7.8",
"@cornerstonejs/tools": "^3.7.8",
"classnames": "^2.3.2"
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.19",
"@cornerstonejs/core": "4.5.19",
"@cornerstonejs/tools": "4.5.19",
"classnames": "2.5.1"
}
}
@@ -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,
@@ -126,7 +86,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { presentationIds } = viewportOptions;
const measurement = srDisplaySet.measurements[newMeasurementSelected];
setPositionPresentation(presentationIds.positionPresentationId, {
viewReference: {
viewReference: measurement.viewReference || {
referencedImageId: measurement.imageId,
},
});
@@ -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,10 +15,14 @@ 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;
type InstanceMetadata = OhifTypes.InstanceMetadata;
/**
* TODO
@@ -30,6 +35,7 @@ const sopClassUids = [
sopClassDictionary.BasicTextSR,
sopClassDictionary.EnhancedSR,
sopClassDictionary.ComprehensiveSR,
sopClassDictionary.Comprehensive3DSR,
];
const validateSameStudyUID = (uid: string, instances): void => {
@@ -48,7 +54,7 @@ const validateSameStudyUID = (uid: string, instances): void => {
* @param instances is a list of instances from THIS series that are not
* in this DICOM SR Display Set already.
*/
function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) {
function addInstances(instances: InstanceMetadata[], _displaySetService: DisplaySetService) {
this.instances.push(...instances);
utils.sortStudyInstances(this.instances);
// The last instance is the newest one, so is the one most interesting.
@@ -63,9 +69,10 @@ function addInstances(instances: InstanceMetadata[], displaySetService: DisplayS
* DICOM SR SOP Class Handler
* For all referenced images in the TID 1500/300 sections, add an image to the
* display.
* @param instances is a set of instances all from the same series
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
* @param {InstanceMetadata[]} instances - A set of instances all from the same series
* @param {AppTypes.ServicesManager} servicesManager - The services that can be used for creating
* @param {AppTypes.ExtensionManager} extensionManager - The extension manager
* @returns {Types.DisplaySet[]} The list of display sets created for the given instances object
*/
function _getDisplaySetsFromSeries(
instances,
@@ -90,6 +97,7 @@ function _getDisplaySetsFromSeries(
SeriesDescription,
SeriesNumber,
SeriesDate,
SeriesTime,
ConceptNameCodeSequence,
SOPClassUID,
} = instance;
@@ -106,6 +114,7 @@ function _getDisplaySetsFromSeries(
SeriesDescription,
SeriesNumber,
SeriesDate,
SeriesTime,
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
@@ -120,6 +129,7 @@ function _getDisplaySetsFromSeries(
sopClassUids,
instance,
addInstances,
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
};
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
@@ -134,7 +144,7 @@ function _getDisplaySetsFromSeries(
* @param extensionManager - The extension manager containing data sources.
*/
async function _load(
srDisplaySet: Types.DisplaySet,
srDisplaySet: OhifTypes.DisplaySet,
servicesManager: AppTypes.ServicesManager,
extensionManager: AppTypes.ExtensionManager
) {
@@ -212,17 +222,15 @@ async function _load(
});
}
/**
* Checks if measurements can be added to a display set.
*
* @param srDisplaySet - The source display set containing measurements.
* @param newDisplaySet - The new display set to check if measurements can be added.
* @param dataSource - The data source used to retrieve image IDs.
* @param servicesManager - The services manager.
*/
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
return (
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
);
}
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
srDisplaySet: OhifTypes.DisplaySet,
newDisplaySet: OhifTypes.DisplaySet,
dataSource,
servicesManager: AppTypes.ServicesManager
) {
@@ -232,18 +240,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 +262,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 +277,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
addSRAnnotation(measurement, null, null);
if (
is3DSR &&
is3DMeasurement &&
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
) {
addSRAnnotation({ measurement, displaySet: newDisplaySet });
measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
continue;
}
@@ -296,15 +303,12 @@ function _checkIfCanAddMeasurementsToDisplaySet(
imageId &&
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
) {
addSRAnnotation(measurement, imageId, frame);
// Update measurement properties
addSRAnnotation({ measurement, imageId, frameNumber: frame, displaySet: newDisplaySet });
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
unloadedMeasurements.splice(j, 1);
}
}
@@ -312,10 +316,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
/**
* Checks if a measurement references a specific SOP Instance UID.
* @param measurement - The measurement object.
* @param SOPInstanceUID - The SOP Instance UID to check against.
* @param frameNumber - The frame number to check against (optional).
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
* @param {any} measurement - The measurement object.
* @param {string} sopInstanceUID - The SOP Instance UID to check against.
* @param {number} frameNumber - The frame number to check against (optional).
* @returns {boolean} True if the measurement references the specified SOP Instance UID, false otherwise.
*/
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
const { coords } = measurement;
@@ -347,12 +351,11 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
/**
* Retrieves the SOP class handler module.
*
* @param {Object} options - The options for retrieving the SOP class handler module.
* @param {Object} options.servicesManager - The services manager.
* @param {Object} options.extensionManager - The extension manager.
* @returns {Array} An array containing the SOP class handler module.
* @param {OhifTypes.Extensions.ExtensionParams} params - The extension parameters.
* @returns {Array} An array containing the SOP class handler modules.
*/
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
};
@@ -373,8 +376,8 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
/**
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
* @returns {Array} - The array of measurements.
* @param {any[]} imagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
* @returns {any[]} The array of measurements.
*/
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
@@ -412,8 +415,8 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
/**
* Retrieves merged content sequences by tracking unique identifiers.
*
* @param {Array} MeasurementGroups - The measurement groups.
* @returns {Object} - The merged content sequences by tracking unique identifiers.
* @param {any[]} measurementGroups - The measurement groups.
* @returns {Object} The merged content sequences by tracking unique identifiers.
*/
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
@@ -460,15 +463,11 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
* it calls the _processTID1410Measurement function.
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
*
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
* @returns {any} - The processed measurement result.
* @param {any[]} mergedContentSequence - The merged content sequence to process.
* @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);
}
@@ -480,8 +479,8 @@ function _processMeasurement(mergedContentSequence) {
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
*
* @param mergedContentSequence - The merged content sequence containing the measurements.
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
* @param {any[]} mergedContentSequence - The merged content sequence containing the measurements.
* @returns {any} The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
*/
function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
@@ -506,12 +505,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 => {
@@ -541,8 +553,8 @@ function _processTID1410Measurement(mergedContentSequence) {
/**
* Processes the non-geometrically defined measurement from the merged content sequence.
*
* @param mergedContentSequence The merged content sequence containing the measurement data.
* @returns The processed measurement object.
* @param {any[]} mergedContentSequence The merged content sequence containing the measurement data.
* @returns {any} The processed measurement object.
*/
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
@@ -562,6 +574,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 +588,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(
@@ -628,8 +654,8 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
/**
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
* @param {object} graphicItem - The graphic item containing the coordinates.
* @returns {object} - The extracted coordinates.
* @param {any} graphicItem - The graphic item containing the coordinates.
* @returns {any} The extracted coordinates.
*/
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
const { ValueType, GraphicType, GraphicData } = graphicItem;
@@ -643,9 +669,9 @@ const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
/**
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
* @returns {Object} - An object containing the label and value.
* @param {any} conceptNameCodeSequence - The ConceptNameCodeSequence object.
* @param {any} measuredValueSequence - The MeasuredValueSequence object.
* @returns {Object} An object containing the label and value.
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
* Example: { label: 'Long Axis', value: '31.00 mm' }
@@ -664,8 +690,8 @@ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredVal
/**
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
*
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
* @returns {Array} - The list of referenced images.
* @param {any[]} imagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
* @returns {any[]} The list of referenced images.
*/
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
@@ -712,7 +738,7 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
* Otherwise, the sequence is wrapped in an array and returned.
*
* @param {any} sequence - The DICOM sequence to convert.
* @returns {any[]} - The converted array.
* @returns {any[]} The converted array.
*/
function _getSequenceAsArray(sequence) {
if (!sequence) {
@@ -721,4 +747,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;
+4 -29
View File
@@ -9,11 +9,10 @@ import {
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Types } from '@ohif/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
@@ -29,7 +28,7 @@ export default function init({
configuration = {},
servicesManager,
}: Types.Extensions.ExtensionParams): void {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
const { measurementService } = servicesManager.services;
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
@@ -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,11 +1,59 @@
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';
export default function addSRAnnotation(measurement, imageId, frameNumber) {
const { MeasurementReport } = adaptersSR.Cornerstone3D;
/**
* Adds a DICOM SR (Structured Report) annotation to the annotation manager.
* This function processes measurement data from DICOM SR and converts it into
* a format suitable for display in the Cornerstone3D viewer.
*
* @param {Object} params - The parameters object
* @param {Object} params.measurement - The DICOM SR measurement data containing coordinates, labels, and metadata
* @param {Array} params.measurement.coords - Array of coordinate objects with GraphicType, ValueType, and other properties
* @param {string} params.measurement.TrackingUniqueIdentifier - Unique identifier for the measurement
* @param {string} params.measurement.TrackingIdentifier - Tracking identifier for adapter lookup
* @param {Array} [params.measurement.labels] - Optional array of label objects
* @param {string} [params.measurement.displayText] - Optional display text for the annotation
* @param {Object} [params.measurement.textBox] - Optional text box configuration
* @param {string|null} [params.imageId] - Optional image ID for the referenced image (defaults to null)
* @param {number|null} [params.frameNumber] - Optional frame number for multi-frame images (defaults to null)
* @param {Object} params.displaySet - The display set containing the image
* @param {string} params.displaySet.displaySetInstanceUID - Unique identifier for the display set
* @returns {void}
*
* @example
* ```typescript
* addSRAnnotation({
* measurement: {
* TrackingUniqueIdentifier: '1.2.3.4.5',
* TrackingIdentifier: 'POINT',
* coords: [{
* GraphicType: 'POINT',
* ValueType: 'SCOORD',
* // ... other coordinate properties
* }],
* labels: [{ value: 'Measurement Point' }],
* displayText: 'Point measurement'
* },
* imageId: 'wadouri:file://path/to/image.dcm', // Optional
* frameNumber: 0, // Optional
* displaySet: { displaySetInstanceUID: '1.2.3.4' }
* });
* ```
*/
export default function addSRAnnotation({ measurement, imageId = null, frameNumber = null, displaySet }) {
/** @type {string} The tool name to use for the annotation, defaults to DICOMSRDisplay */
let toolName = toolNames.DICOMSRDisplay;
/**
* @type {Object} Renderable data organized by graphic type
* Groups coordinate data by GraphicType for efficient rendering
*/
const renderableData = measurement.coords.reduce((acc, coordProps) => {
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
@@ -18,6 +66,7 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
let frameOfReferenceUID = null;
let planeRestriction = null;
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
@@ -25,23 +74,47 @@ 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;
planeRestriction = {
FrameOfReferenceUID: frameOfReferenceUID,
point: graphicTypePoints[0][0],
};
}
// Store the view reference for use in initial navigation
measurement.viewReference = {
planeRestriction,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
};
/**
* @type {Types.Annotation} The annotation object to be added to the annotation manager
* Contains all necessary metadata and data for rendering the DICOM SR measurement
*/
const SRAnnotation: Types.Annotation = {
annotationUID: TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
isPreview: toolName === toolNames.DICOMSRDisplay,
invalidated: false,
metadata: {
toolName,
planeRestriction,
valueType,
graphicType,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
},
data: {
label: measurement.labels?.[0]?.value || undefined,
@@ -59,9 +132,11 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
};
/**
* const annotationManager = annotation.annotationState.getAnnotationManager();
* was not triggering annotation_added events.
* Add the annotation to the annotation state manager.
* Note: Using annotation.state.addAnnotation() instead of annotationManager.addAnnotation()
* because the latter was not triggering annotation_added events properly.
*
* @param {Types.Annotation} SRAnnotation - The annotation to add
*/
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,
@@ -71,24 +74,20 @@ export default function hydrateStructuredReport(
);
const sopInstanceUIDToImageId = {};
const imageIdsForToolState = {};
displaySet.measurements.forEach(measurement => {
const { ReferencedSOPInstanceUID, imageId, frameNumber } = measurement;
const { ReferencedSOPInstanceUID, imageId, frameNumber=1 } = measurement;
const key = `${ReferencedSOPInstanceUID}:${frameNumber}`;
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
imageIdsForToolState[ReferencedSOPInstanceUID] = [];
}
if (!imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber]) {
imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber] = imageId;
if (!sopInstanceUIDToImageId[key]) {
sopInstanceUIDToImageId[key] = imageId;
}
});
// 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 +95,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
);
@@ -131,10 +129,8 @@ export default function hydrateStructuredReport(
// 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];
const frameNumber = toolData.annotation.data?.frameNumber || 1;
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
@@ -147,6 +143,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 +159,53 @@ 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 = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
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 +239,7 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
@@ -238,3 +252,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.136",
"version": "3.12.0-beta.76",
"description": "OHIF extension for 4D volumes data",
"author": "OHIF",
"license": "MIT",
@@ -29,21 +29,21 @@
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.136",
"@ohif/extension-cornerstone": "3.10.0-beta.136",
"@ohif/extension-default": "3.10.0-beta.136",
"@ohif/i18n": "3.10.0-beta.136",
"@ohif/ui": "3.10.0-beta.136",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1"
"@ohif/core": "3.12.0-beta.76",
"@ohif/extension-cornerstone": "3.12.0-beta.76",
"@ohif/extension-default": "3.12.0-beta.76",
"@ohif/i18n": "3.12.0-beta.76",
"@ohif/ui": "3.12.0-beta.76",
"dcmjs": "0.43.1",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
"react": "18.3.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.7.8",
"@cornerstonejs/tools": "^3.7.8",
"classnames": "^2.3.2"
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.5.19",
"@cornerstonejs/tools": "4.5.19",
"classnames": "2.5.1"
}
}
@@ -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
+6 -2
View File
@@ -1,9 +1,13 @@
const base = require('../../jest.config.base.js');
const pkg = require('./package');
module.exports = {
...base,
displayName: pkg.name,
moduleNameMapper: {
...base.moduleNameMapper,
'@ohif/(.*)': '<rootDir>/../../platform/$1/src',
'^@cornerstonejs/([^/]+)/(.*)$': '<rootDir>/../../node_modules/@cornerstonejs/$1/dist/esm/$2',
'^@cornerstonejs/([^/]+)$': '<rootDir>/../../node_modules/@cornerstonejs/$1/dist/esm',
},
// rootDir: "../.."
// testMatch: [
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
+33 -30
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.10.0-beta.136",
"version": "3.12.0-beta.76",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -31,40 +31,43 @@
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build",
"start": "yarn run dev"
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@cornerstonejs/codec-charls": "^1.2.3",
"@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.8",
"@ohif/core": "3.10.0-beta.136",
"@ohif/ui": "3.10.0-beta.136",
"dcmjs": "*",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-resize-detector": "^10.0.1"
"@cornerstonejs/codec-charls": "1.2.3",
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.5.19",
"@ohif/core": "3.12.0-beta.76",
"@ohif/ui": "3.12.0-beta.76",
"dcmjs": "0.43.1",
"dicom-parser": "1.8.21",
"hammerjs": "2.0.8",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-resize-detector": "10.0.1"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.7.8",
"@cornerstonejs/core": "^3.7.8",
"@cornerstonejs/labelmap-interpolation": "^3.7.8",
"@cornerstonejs/polymorphic-segmentation": "^3.7.8",
"@cornerstonejs/tools": "^3.7.8",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.19",
"@cornerstonejs/ai": "4.5.19",
"@cornerstonejs/core": "4.5.19",
"@cornerstonejs/labelmap-interpolation": "4.5.19",
"@cornerstonejs/polymorphic-segmentation": "4.5.19",
"@cornerstonejs/tools": "4.5.19",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
"lodash.compact": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.flatten": "^4.4.0",
"lodash.merge": "^4.6.2",
"lodash.zip": "^4.2.0",
"shader-loader": "^1.3.1",
"worker-loader": "^3.0.8"
"html2canvas": "1.4.1",
"lodash.compact": "3.0.1",
"lodash.debounce": "4.0.8",
"lodash.flatten": "4.4.0",
"lodash.merge": "4.6.2",
"lodash.zip": "4.2.0",
"shader-loader": "1.3.1",
"worker-loader": "3.0.8"
}
}
@@ -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,11 @@
import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames';
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import { metaData, Enums, getEnabledElement } 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({
@@ -16,49 +16,36 @@ function ViewportOrientationMarkers({
servicesManager,
orientationMarkers = ['top', 'left'],
}: withAppTypes) {
// Rotation is in degrees
const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false);
const [cameraModifiedTime, setCameraModifiedTime] = useState(0);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
const { cornerstoneViewportService } = servicesManager.services;
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { previousCamera, camera } = evt.detail;
const { rotation } = camera;
if (rotation !== undefined) {
setRotation(rotation);
}
if (
camera.flipHorizontal !== undefined &&
previousCamera.flipHorizontal !== camera.flipHorizontal
) {
setFlipHorizontal(camera.flipHorizontal);
}
if (
camera.flipVertical !== undefined &&
previousCamera.flipVertical !== camera.flipVertical
) {
setFlipVertical(camera.flipVertical);
}
};
const cameraModifiedListener = () => setCameraModifiedTime(Date.now());
element.addEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
};
}, []);
}, [element]);
const markers = useMemo(() => {
if (!viewportData) {
if (!viewportData || cameraModifiedTime === 0) {
return '';
}
if (!element || !getEnabledElement(element)) {
console.log(`ViewportOrientationMarkers :: Viewport element not enabled (${viewportId})`);
return '';
}
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
console.log(`ViewportOrientationMarkers :: No viewport (${viewportId})`);
return '';
}
let rowCosines, columnCosines, isDefaultValueSetForRowCosine, isDefaultValueSetForColumnCosine;
if (viewportData.viewportType === 'stack') {
const imageIndex = imageSliceData.imageIndex;
const imageId = viewportData.data[0].imageIds?.[imageIndex];
@@ -68,59 +55,34 @@ function ViewportOrientationMarkers({
return false;
}
({
rowCosines,
columnCosines,
isDefaultValueSetForColumnCosine,
isDefaultValueSetForColumnCosine,
} = metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
const { isDefaultValueSetForRowCosine, isDefaultValueSetForColumnCosine } =
metaData.get('imagePlaneModule', imageId) || {};
if (isDefaultValueSetForColumnCosine || isDefaultValueSetForRowCosine) {
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 (
!rowCosines ||
!columnCosines ||
rotation === undefined ||
isDefaultValueSetForRowCosine ||
isDefaultValueSetForColumnCosine
) {
return '';
}
const { viewport } = getEnabledElement(element);
const p00 = viewport.canvasToWorld([0, 0]);
const p10 = viewport.canvasToWorld([1, 0]);
const p01 = viewport.canvasToWorld([0, 1]);
const rowCosines = vec3.sub(vec3.create(), p10, p00);
const columnCosines = vec3.sub(vec3.create(), p01, p00);
const markers = _getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
);
vec3.normalize(rowCosines, rowCosines);
vec3.normalize(columnCosines, columnCosines);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
if (!ohifViewport) {
console.log('ViewportOrientationMarkers::No viewport');
return null;
}
const markers = _getOrientationMarkers(rowCosines, columnCosines);
return orientationMarkers.map((m, index) => (
<div
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`}
@@ -128,15 +90,7 @@ function ViewportOrientationMarkers({
<div className="orientation-marker-value">{markers[m]}</div>
</div>
));
}, [
viewportData,
imageSliceData,
rotation,
flipVertical,
flipHorizontal,
orientationMarkers,
element,
]);
}, [viewportData, imageSliceData, cameraModifiedTime, orientationMarkers, element, isLight]);
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
}
@@ -148,10 +102,8 @@ function ViewportOrientationMarkers({
*
* @param {*} rowCosines
* @param {*} columnCosines
* @param {*} rotation in degrees
* @returns
*/
function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertical, flipHorizontal) {
function _getOrientationMarkers(rowCosines, columnCosines) {
const rowString = getOrientationStringLPS(rowCosines);
const columnString = getOrientationStringLPS(columnCosines);
const oppositeRowString = invertOrientationStringLPS(rowString);
@@ -164,43 +116,6 @@ function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertica
bottom: columnString,
};
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
// the rotation applications
if (flipVertical) {
markers.top = invertOrientationStringLPS(markers.top);
markers.bottom = invertOrientationStringLPS(markers.bottom);
}
if (flipHorizontal) {
markers.left = invertOrientationStringLPS(markers.left);
markers.right = invertOrientationStringLPS(markers.right);
}
// Swap the labels accordingly if the viewport has been rotated
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
if (rotation === 90 || rotation === -270) {
return {
top: markers.left,
left: invertOrientationStringLPS(markers.top),
right: invertOrientationStringLPS(markers.bottom),
bottom: markers.right, // left
};
} else if (rotation === -90 || rotation === 270) {
return {
top: invertOrientationStringLPS(markers.left),
left: markers.top,
bottom: markers.left,
right: markers.bottom,
};
} else if (rotation === 180 || rotation === -180) {
return {
top: invertOrientationStringLPS(markers.top),
left: invertOrientationStringLPS(markers.left),
bottom: invertOrientationStringLPS(markers.bottom),
right: invertOrientationStringLPS(markers.right),
};
}
return markers;
}
File diff suppressed because it is too large. Load diff
@@ -55,7 +55,7 @@ const DEFAULT_TYPES = [GroupAccordion, Content, Trigger];
* measurements panel for a practical, working example.
*/
export function AccordionGroup(props) {
const { grouping, items, children, sourceChildren, type } = props;
const { grouping, items, children, sourceChildren } = props;
const childProps = useSystem();
let defaultValue = props.defaultValue;
const groups = grouping.groupingFunction(items, grouping, childProps);
@@ -179,7 +179,10 @@ function Trigger(props) {
>
<div>
{React.cloneElement(children, childProps)}
<ChevronDownIcon key="chevronDown" className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDownIcon
key="chevronDown"
className="text-primary h-4 w-4 shrink-0 transition-transform duration-200"
/>
</div>
</AccordionTrigger>
);
@@ -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>
@@ -54,7 +54,7 @@ export const groupByNamedSets = (items, grouping) => {
};
export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
const { items, children, grouping = {}, customHeader, group } = props;
const { items, children, grouping = {}, customHeader, group, actions } = props;
return (
<AccordionGroup
@@ -72,6 +72,7 @@ export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
<MeasurementTableNested
customHeader={customHeader}
allItems={items}
actions={actions}
/>
</AccordionGroup.Accordion>
</AccordionGroup>
@@ -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,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;
Loaded 100 of 1851 files, more files were not shown because too many files have changed in this diff. Show more