Compare commits

..
Author SHA1 Message Date
ohif-bot 9ad7623a25 chore(version): Update package versions [skip ci] 2025-10-29 18:22:19 +00:00
ohif-bot eb564aec5e chore(version): version.json [skip ci] 2025-10-29 18:22:11 +00:00
35bfc9b0cc feature(segmentation): Add contour tools and utilities in a side panel (#5517)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2025-10-29 14:15:52 -04:00
ohif-bot ab440f086a chore(version): Update package versions [skip ci] 2025-10-28 12:57:15 +00:00
ohif-bot 1d33bcf229 chore(version): version.json [skip ci] 2025-10-28 12:57:08 +00:00
Vinícius Alves de Faria Resende 76f7d4e23a fix(3DSegmentation): [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout (#5505) 2025-10-28 08:52:12 -04:00
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
435 changed files with 36324 additions and 9432 deletions

No files matched your search

+30 -11
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
@@ -352,7 +371,7 @@ jobs:
name: Export Display Variable
command: export DISPLAY=:99
- cypress/install:
install-command: yarn install --no-save
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
+2
View File
@@ -23,6 +23,8 @@ jobs:
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
+3 -1
View File
@@ -1,7 +1,7 @@
name: Playwright Tests
on:
pull_request:
branches: [master]
branches: [master, release/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -18,6 +18,8 @@ jobs:
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: ${{ matrix.node-version }}
+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"
}
}
+554
View File
@@ -3,6 +3,560 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
### Bug Fixes
* **security:** Addressed dependabot alert CVE-2025-59288 concerning playwright ([#5527](https://github.com/OHIF/Viewers/issues/5527)) ([1f11c83](https://github.com/OHIF/Viewers/commit/1f11c83006bff98cb8434f0662b4de55d5b9bf78))
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
### Bug Fixes
* **security:** For bun, enforce frozen lockfile. For yarn, strongly suggest using frozen lockfile. ([#5508](https://github.com/OHIF/Viewers/issues/5508)) ([1009c60](https://github.com/OHIF/Viewers/commit/1009c6091107d2db0768622120f916208a391343))
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
### Features
* **jpeg2000:** Add 16-bit RGB support to JPEG2000 decoder ([#5519](https://github.com/OHIF/Viewers/issues/5519)) ([a154443](https://github.com/OHIF/Viewers/commit/a1544432db42bf77864bb7df2f757cae819a4b4d))
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
### Bug Fixes
* Fixed the tooltip text of the window button, it was mistakenly written in Chinese in the en-us json ([#5513](https://github.com/OHIF/Viewers/issues/5513)) ([8b9445c](https://github.com/OHIF/Viewers/commit/8b9445c442d0a40b7e4393614313192bd006461b))
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
### Bug Fixes
* **segmentation:** Fixed the manual scrolling of the segments list and implemented automatic scrolling to the active segment. ([#5510](https://github.com/OHIF/Viewers/issues/5510)) ([1df4f84](https://github.com/OHIF/Viewers/commit/1df4f843fe3c377643938563ff9e2e4f930d8537))
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
### Bug Fixes
* **rendering:** Fix palette color LUT conversion causing black images ([#5509](https://github.com/OHIF/Viewers/issues/5509)) ([ffd9ec3](https://github.com/OHIF/Viewers/commit/ffd9ec32730580e266ffae365a10ac3e524dd6cf))
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
### Bug Fixes
* **ErrorBoundary:** Allow for details to be shown in production. ([#5504](https://github.com/OHIF/Viewers/issues/5504)) ([4620cc3](https://github.com/OHIF/Viewers/commit/4620cc3acf89f218c3feaaf4c153a3dc8b023b23))
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
### Bug Fixes
* **docker:** Use bun version 1.2.23 for docker build. ([#5501](https://github.com/OHIF/Viewers/issues/5501)) ([86e924f](https://github.com/OHIF/Viewers/commit/86e924fb88cc146f91c7d826ee071e451de47942))
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
### Bug Fixes
* Segmentation not displayed and unable to draw segments in volume viewport after converting Labelmap to Surface ([#5488](https://github.com/OHIF/Viewers/issues/5488)) ([faea09e](https://github.com/OHIF/Viewers/commit/faea09e7ef2667aad5cef1efc2a6516677ca2915))
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
### Bug Fixes
* **segmentationService:** prevent no representation crash ([#5495](https://github.com/OHIF/Viewers/issues/5495)) ([3137aed](https://github.com/OHIF/Viewers/commit/3137aed0170196a61e16c6dac0c15bcc41691919))
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
### Bug Fixes
* **docs:** Set exact version for glob. ([#5497](https://github.com/OHIF/Viewers/issues/5497)) ([0d2f4e7](https://github.com/OHIF/Viewers/commit/0d2f4e715a941fa458ebf8e64972088580a04952))
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
### Bug Fixes
* **security:** Run bun audit before bun install. ([#5496](https://github.com/OHIF/Viewers/issues/5496)) ([fbd9371](https://github.com/OHIF/Viewers/commit/fbd9371884d0e87376d67eb98c17a4d62e4e6dd1))
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
### Bug Fixes
* **bun:** Use fixed bun version ([#5493](https://github.com/OHIF/Viewers/issues/5493)) ([3fe67d5](https://github.com/OHIF/Viewers/commit/3fe67d5c98a22edc32cb0d3de5edc05b722dd68e))
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
### Bug Fixes
* Update Dockerfile ([#5484](https://github.com/OHIF/Viewers/issues/5484)) ([41c0287](https://github.com/OHIF/Viewers/commit/41c028781b80f0ca80f4d6cde3ca8e7958f1baed))
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
### Bug Fixes
* **segmentation:** Segmentation not displayed and unable to draw segments when switching back to volume/stack viewport from 3D viewport ([#5430](https://github.com/OHIF/Viewers/issues/5430)) ([78df8ff](https://github.com/OHIF/Viewers/commit/78df8ff03a371904a390c63a95628d5511dad136))
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
### Bug Fixes
* validation for Percentage of Max SUV input field in TMTV module ([#5417](https://github.com/OHIF/Viewers/issues/5417)) ([5040c94](https://github.com/OHIF/Viewers/commit/5040c947d2f47e5ec66285fa353e3e0ffd127ba7))
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
### Bug Fixes
* **Segmentation:** [Bug [#5420](https://github.com/OHIF/Viewers/issues/5420)] Segmentation color resets after using toggleOneUp ([#5465](https://github.com/OHIF/Viewers/issues/5465)) ([2c6ce21](https://github.com/OHIF/Viewers/commit/2c6ce21c8f79f8b3e6dc47b5aa6f717b6b5cbd2a))
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
### Bug Fixes
* **measurementTrackingMachine:** remove unreachable states ([#5460](https://github.com/OHIF/Viewers/issues/5460)) ([7f12f39](https://github.com/OHIF/Viewers/commit/7f12f398fb145a31e5557dbee51c0a9b0f40a35b))
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
### Bug Fixes
* **toolbar:** prevent duplicate command execution on repeated interactions ([#5456](https://github.com/OHIF/Viewers/issues/5456)) ([4d04dd5](https://github.com/OHIF/Viewers/commit/4d04dd5651f4cf8dcaf4732baee5e218e92bd3c1))
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
### Bug Fixes
* viewport ready event firing logic ([#5462](https://github.com/OHIF/Viewers/issues/5462)) ([d2ae13b](https://github.com/OHIF/Viewers/commit/d2ae13b03f21e3ab9545d50d7403ec52004bfbfc))
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
### Bug Fixes
* **router:** opt in to React Router v7 future flags ([#5461](https://github.com/OHIF/Viewers/issues/5461)) ([e2964bf](https://github.com/OHIF/Viewers/commit/e2964bf66947d18af06f292d9395b465545fdf9d))
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
### Bug Fixes
* Safely handle missing active viewport ([#5448](https://github.com/OHIF/Viewers/issues/5448)) ([792db26](https://github.com/OHIF/Viewers/commit/792db26ff1591f9e054486f1bee01c422e774101))
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
### Features
* **MeasurementService:** add rendering of unmapped measurements ([#5416](https://github.com/OHIF/Viewers/issues/5416)) ([851e74d](https://github.com/OHIF/Viewers/commit/851e74d7b867a806befb5d85fd71ff9a75e9f2d2))
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
### Bug Fixes
* duplicate segmentation entries on layout switch ([#5458](https://github.com/OHIF/Viewers/issues/5458)) ([9e951e8](https://github.com/OHIF/Viewers/commit/9e951e88005af505ae0c07fbcd3366de12b773dd))
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
### Bug Fixes
* study browser mode for multiple studies ([#5457](https://github.com/OHIF/Viewers/issues/5457)) ([6a1df9d](https://github.com/OHIF/Viewers/commit/6a1df9de4ad3c10a4653f18e03aff6bda4962fd1))
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
### Bug Fixes
* **studyBrowserCustomization:** fix thumbnail menu item actions in study browser ([#5283](https://github.com/OHIF/Viewers/issues/5283)) ([2f28a0d](https://github.com/OHIF/Viewers/commit/2f28a0deab5e43f432bbd9b5989928c9b0450278))
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
### Features
* Add inherit from basic/longitudinal modes ([#5435](https://github.com/OHIF/Viewers/issues/5435)) ([9ad0d7f](https://github.com/OHIF/Viewers/commit/9ad0d7fc8cf34867fa2cfc2bf2b2f63451ec03e5))
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
### Bug Fixes
* **segmentation:** Restored segment label tool in longitudinal mode. ([#5443](https://github.com/OHIF/Viewers/issues/5443)) ([dd86a8e](https://github.com/OHIF/Viewers/commit/dd86a8eabd0712d25e7652a242377fe09597e8d5))
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
### Bug Fixes
* **security:** Removed dependency on tar-fs by removing dependencies on storybook and sharp. ([#5438](https://github.com/OHIF/Viewers/issues/5438)) ([80f314a](https://github.com/OHIF/Viewers/commit/80f314a422ec45fff6cdd5869f4293a110d00125))
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
### Bug Fixes
* Improve rt handling for bulkdata ([#5415](https://github.com/OHIF/Viewers/issues/5415)) ([d88a341](https://github.com/OHIF/Viewers/commit/d88a341884f7361865bc10159c1a82633667ab5b))
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
### Bug Fixes
* **i18n:** Add and update i18n Translation for Toolbar Tool Names ([#5392](https://github.com/OHIF/Viewers/issues/5392)) ([7783d0f](https://github.com/OHIF/Viewers/commit/7783d0f558bbb7016c29513b6fe76ed069dbd75c))
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
### Features
* **Segmentation:** Segmentation highlight animation function selection ([#5401](https://github.com/OHIF/Viewers/issues/5401)) ([69dbe27](https://github.com/OHIF/Viewers/commit/69dbe2778493cdfc7a8da5e757c95217bb31edc2))
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
### Bug Fixes
* **SegmentationService:** _getSegmentCenter wrongly handling seg center availability ([#5396](https://github.com/OHIF/Viewers/issues/5396)) ([74b82bd](https://github.com/OHIF/Viewers/commit/74b82bd93442411ad68f4872992126c40321eb9e))
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package ohif-monorepo-root
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
### Bug Fixes
* **ErrorBoundary:** Fixing console errors and Improving error logging for better developer experience ([#5378](https://github.com/OHIF/Viewers/issues/5378)) ([56c5828](https://github.com/OHIF/Viewers/commit/56c5828b02d53bfd17182bb8d0c126b7cd4f400e))
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
+3 -2
View File
@@ -49,7 +49,7 @@ 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 bun@1.2.23
RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
@@ -60,6 +60,7 @@ 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 . .
@@ -98,7 +99,7 @@ COPY --from=builder /usr/src/app/platform/app/dist/dicom-microscopy-viewer /usr/
# 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 && chmod -R 666 /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 -3
View File
@@ -76,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
@@ -147,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
@@ -158,7 +165,7 @@ _From this repository's root directory:_
yarn config set workspaces-experimental true
# Restore dependencies
yarn install
yarn install --frozen-lockfile
```
## Commands
+446
View File
@@ -3,6 +3,452 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
### Bug Fixes
* **security:** Removed dependency on tar-fs by removing dependencies on storybook and sharp. ([#5438](https://github.com/OHIF/Viewers/issues/5438)) ([80f314a](https://github.com/OHIF/Viewers/commit/80f314a422ec45fff6cdd5869f4293a110d00125))
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @externals/devDependencies
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @externals/devDependencies
+67 -67
View File
@@ -1,7 +1,7 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"license": "MIT",
"private": true,
"engines": {
@@ -9,85 +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.30.1"
"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": {
"@babel/eslint-parser": "7.28.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@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.15",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"autoprefixer": "^10.4.4",
"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",
"@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.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-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.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",
"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.5.1",
"serve": "^14.2.4",
"shader-loader": "^1.3.1",
"shx": "^0.3.3",
"source-map-loader": "^4.0.1",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^5.1.4",
"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": "^5.0.1",
"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.25.0",
"webpack-merge": "^5.7.3",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
"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"
+443
View File
@@ -3,6 +3,449 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "^0.48.6"
"dicom-microscopy-viewer": "0.48.6"
}
}
+17 -18
View File
@@ -22,30 +22,29 @@
},
"devDependencies": {
"@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.16.7",
"@babel/plugin-transform-regenerator": "^7.16.7",
"@babel/plugin-transform-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/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1"
"@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.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"
}
}
+1
View File
@@ -35,6 +35,7 @@ module.exports = {
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-class-static-block',
'@babel/plugin-transform-for-of',
['babel-plugin-transform-import-meta', { module: 'ES6' }],
],
},
production: {
+779 -1682
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 @@
d6ea179ac43a47d4011b8dcaf59d79d99382d6c8
35bfc9b0ccfea25a61bebf8e252284db7c40e602
@@ -3,6 +3,458 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"description": "DICOM Parametric Map read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.12.0-beta.23",
"@ohif/extension-cornerstone": "3.12.0-beta.23",
"@ohif/extension-default": "3.12.0-beta.23",
"@ohif/i18n": "3.12.0-beta.23",
"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.78",
"@ohif/extension-cornerstone": "3.12.0-beta.78",
"@ohif/extension-default": "3.12.0-beta.78",
"@ohif/i18n": "3.12.0-beta.78",
"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": "^4.0.0",
"@cornerstonejs/core": "^4.0.0",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.20",
"@cornerstonejs/core": "4.5.20",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
"react-color": "2.19.3"
}
}
@@ -3,6 +3,452 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
### Bug Fixes
* Improve rt handling for bulkdata ([#5415](https://github.com/OHIF/Viewers/issues/5415)) ([d88a341](https://github.com/OHIF/Viewers/commit/d88a341884f7361865bc10159c1a82633667ab5b))
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"description": "DICOM RT read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,19 +33,19 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.12.0-beta.23",
"@ohif/extension-cornerstone": "3.12.0-beta.23",
"@ohif/extension-default": "3.12.0-beta.23",
"@ohif/i18n": "3.12.0-beta.23",
"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.78",
"@ohif/extension-cornerstone": "3.12.0-beta.78",
"@ohif/extension-default": "3.12.0-beta.78",
"@ohif/i18n": "3.12.0-beta.78",
"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"
}
}
@@ -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,7 +196,6 @@ 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;
}
@@ -185,6 +248,18 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
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,
@@ -220,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;
@@ -233,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
@@ -252,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];
}
@@ -3,6 +3,458 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"description": "DICOM SEG read workflow",
"author": "OHIF",
"license": "MIT",
@@ -33,22 +33,22 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "3.12.0-beta.23",
"@ohif/extension-cornerstone": "3.12.0-beta.23",
"@ohif/extension-default": "3.12.0-beta.23",
"@ohif/i18n": "3.12.0-beta.23",
"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.78",
"@ohif/extension-cornerstone": "3.12.0-beta.78",
"@ohif/extension-default": "3.12.0-beta.78",
"@ohif/i18n": "3.12.0-beta.78",
"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": "^4.0.0",
"@cornerstonejs/core": "^4.0.0",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.20",
"@cornerstonejs/core": "4.5.20",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
"react-color": "2.19.3"
}
}
@@ -3,7 +3,7 @@ import { classes, Types } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core';
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
import { createReportDialogPrompt } from '@ohif/extension-default';
import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default';
import { DicomMetadataStore } from '@ohif/core';
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
@@ -37,7 +37,7 @@ const commandsModule = ({
servicesManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const { segmentationService, displaySetService, viewportGridService, toolGroupService } =
const { segmentationService, displaySetService, viewportGridService } =
servicesManager.services as AppTypes.Services;
const actions = {
@@ -307,6 +307,17 @@ const commandsModule = ({
console.warn(e);
}
},
toggleActiveSegmentationUtility: ({ itemId: buttonId }) => {
const { uiState, setUIState } = useUIStateStore.getState();
const isButtonActive = uiState['activeSegmentationUtility'] === buttonId;
console.log('toggleActiveSegmentationUtility', isButtonActive, buttonId);
// if the button is active, clear the active segmentation utility
if (isButtonActive) {
setUIState('activeSegmentationUtility', null);
} else {
setUIState('activeSegmentationUtility', buttonId);
}
},
};
const definitions = {
@@ -326,6 +337,9 @@ const commandsModule = ({
downloadRTSS: {
commandFn: actions.downloadRTSS,
},
toggleActiveSegmentationUtility: {
commandFn: actions.toggleActiveSegmentationUtility,
},
};
return {
@@ -0,0 +1,243 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useRunCommand, useSystem } from '@ohif/core';
import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone';
import {
Button,
cn,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Switch,
Tabs,
TabsList,
TabsTrigger,
} from '@ohif/ui-next';
import { Icons } from '@ohif/ui-next';
import { contourSegmentation } from '@cornerstonejs/tools/utilities';
import { Segment } from '@cornerstonejs/tools/types';
const { LogicalOperation } = contourSegmentation;
const options = [
{
value: 'merge',
logicalOperation: LogicalOperation.Union,
label: 'Merge',
icon: 'actions-combine-merge',
helperIcon: 'helper-combine-merge',
},
{
value: 'intersect',
logicalOperation: LogicalOperation.Intersect,
label: 'Intersect',
icon: 'actions-combine-intersect',
helperIcon: 'helper-combine-intersect',
},
{
value: 'subtract',
logicalOperation: LogicalOperation.Subtract,
label: 'Subtract',
icon: 'actions-combine-subtract',
helperIcon: 'helper-combine-subtract',
},
];
// Shared component for segment selection
function SegmentSelector({
label,
value,
onValueChange,
segments,
placeholder = 'Select a segment',
}: {
label: string;
value: string;
onValueChange: (value: string) => void;
segments: Segment[];
placeholder?: string;
}) {
return (
<div className="flex justify-between gap-6">
<div>{label}</div>
<Select
key={`select-segment-${label}`}
onValueChange={onValueChange}
value={value}
>
<SelectTrigger className="overflow-hidden">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{segments.map(segment => (
<SelectItem
key={segment.segmentIndex}
value={segment.segmentIndex.toString()}
>
{segment.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
function LogicalContourOperationOptions() {
const { servicesManager } = useSystem();
const { segmentationService } = servicesManager.services;
const { segmentationsWithRepresentations } = useActiveViewportSegmentationRepresentations();
const activeRepresentation = segmentationsWithRepresentations?.find(
({ representation }) => representation?.active
);
const segments = activeRepresentation
? Object.values(activeRepresentation.segmentation.segments)
: [];
// Calculate the next available segment index
const nextSegmentIndex = activeRepresentation
? segmentationService.getNextAvailableSegmentIndex(
activeRepresentation.segmentation.segmentationId
)
: 1;
const activeSegment = segments.find(segment => segment.active);
const activeSegmentIndex = activeSegment?.segmentIndex || 0;
const [operation, setOperation] = useState(options[0]);
const [segmentA, setSegmentA] = useState<string>(activeSegmentIndex?.toString() || '');
const [segmentB, setSegmentB] = useState<string>('');
const [createNewSegment, setCreateNewSegment] = useState<boolean>(false);
const [newSegmentName, setNewSegmentName] = useState<string>('');
useEffect(() => {
setSegmentA(activeSegmentIndex?.toString() || null);
}, [activeSegmentIndex]);
useEffect(() => {
setNewSegmentName(`Segment ${nextSegmentIndex}`);
}, [nextSegmentIndex]);
const runCommand = useRunCommand();
const applyLogicalContourOperation = useCallback(() => {
let resultSegmentIndex = segmentA;
if (createNewSegment) {
resultSegmentIndex = nextSegmentIndex.toString();
runCommand('addSegment', {
segmentationId: activeRepresentation.segmentation.segmentationId,
config: {
label: newSegmentName,
segmentIndex: nextSegmentIndex,
},
});
}
runCommand('applyLogicalContourOperation', {
segmentAInfo: {
segmentationId: activeRepresentation.segmentation.segmentationId,
segmentIndex: parseInt(segmentA),
},
segmentBInfo: {
segmentationId: activeRepresentation.segmentation.segmentationId,
segmentIndex: parseInt(segmentB),
},
resultSegmentInfo: {
segmentationId: activeRepresentation.segmentation.segmentationId,
segmentIndex: parseInt(resultSegmentIndex),
},
logicalOperation: operation.logicalOperation,
});
}, [
activeRepresentation?.segmentation?.segmentationId,
createNewSegment,
newSegmentName,
nextSegmentIndex,
operation.logicalOperation,
runCommand,
segmentA,
segmentB,
]);
return (
<div className="flex w-[245px] flex-col gap-4">
<div className="flex items-start justify-between">
<div className="flex w-auto flex-col items-center gap-2 text-base font-normal leading-none">
<Tabs value={operation.value}>
<TabsList className="inline-flex space-x-1">
{options.map(option => {
const { value, icon } = option;
return (
<TabsTrigger
value={value}
key={`logical-contour-operation-${value}`}
onClick={() => setOperation(option)}
>
<Icons.ByName name={icon}></Icons.ByName>
</TabsTrigger>
);
})}
</TabsList>
</Tabs>
<div>{operation.label}</div>
</div>
<div className="bg-primary-dark flex h-[62px] w-[88px] items-center justify-center rounded-lg">
<Icons.ByName name={operation.helperIcon}></Icons.ByName>
</div>
</div>
<SegmentSelector
label="A"
value={segmentA}
onValueChange={setSegmentA}
segments={segments}
/>
<SegmentSelector
label="B"
value={segmentB}
onValueChange={setSegmentB}
segments={segments}
/>
<div className="flex justify-end pl-[34px]">
<Button
className="border-primary/60 grow border"
variant="ghost"
onClick={() => {
applyLogicalContourOperation();
}}
>
{operation.label}
</Button>
</div>
<Separator className="bg-input mt-2 h-[1px]" />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-start gap-2">
<Switch
id="logical-contour-operations-create-new-segment-switch"
onCheckedChange={setCreateNewSegment}
></Switch>
<Label htmlFor="logical-contour-operations-create-new-segment-switch">
Create a new segment
</Label>
</div>
<div className="pl-9">
<Input
className={cn(createNewSegment ? 'visible' : 'hidden')}
disabled={!createNewSegment}
id="logical-contour-operations-create-new-segment-input"
type="text"
placeholder="New segment name"
value={newSegmentName}
onChange={e => setNewSegmentName(e.target.value)}
/>
</div>
</div>
</div>
);
}
export default LogicalContourOperationOptions;
@@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { Button, Input, Label, Separator } from '@ohif/ui-next';
import { useRunCommand } from '@ohif/core';
function SimplifyContourOptions() {
const [areaThreshold, setAreaThreshold] = useState(10);
const runCommand = useRunCommand();
return (
<div className="flex w-auto w-[252px] flex-col gap-[8px] text-base font-normal leading-none">
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
<div>Fill contour holes</div>
<Button
className="border-primary/60 border"
variant="ghost"
onClick={() => {
runCommand('removeContourHoles');
}}
>
Fill Holes
</Button>
<Separator className="bg-input mt-[20px] h-[1px]" />
</div>
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
<div>Remove Small Contours</div>
<div className="flex items-center gap-2 self-end">
<Label
htmlFor="simplify-contour-options"
className="text-muted-foreground"
>
Area Threshold
</Label>
<Input
id="simplify-contour-options"
className="w-20"
type="number"
value={areaThreshold}
onChange={e => setAreaThreshold(Number(e.target.value))}
/>
</div>
<Button
className="border-primary/60 border"
variant="ghost"
onClick={() => {
runCommand('removeSmallContours', {
areaThreshold,
});
}}
>
Remove Small Contours
</Button>
<Separator className="bg-input mt-[20px] h-[1px]" />
</div>
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
<div>Create New Segment from Holes</div>
<Button
className="border-primary/60 border"
variant="ghost"
onClick={() => {
runCommand('convertContourHoles');
}}
>
Create New Segment
</Button>
</div>
</div>
);
}
export default SimplifyContourOptions;
@@ -0,0 +1,39 @@
import React from 'react';
import { Button, Separator } from '@ohif/ui-next';
import { useRunCommand } from '@ohif/core';
function SmoothContoursOptions() {
const runCommand = useRunCommand();
return (
<div className="flex w-auto w-[245px] flex-col gap-[8px] text-base font-normal leading-none">
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
<div>Smooth all edges</div>
<Button
className="border-primary/60 border"
variant="ghost"
onClick={() => {
runCommand('smoothContours');
}}
>
Smooth Edges
</Button>
<Separator className="bg-input mt-[20px] h-[1px]" />
</div>
<div className="flex w-auto flex-col gap-[10px] text-base font-normal leading-none">
<div>Remove extra points</div>
<Button
className="border-primary/60 border"
variant="ghost"
onClick={() => {
runCommand('decimateContours');
}}
>
Remove Points
</Button>
</div>
</div>
);
}
export default SmoothContoursOptions;
@@ -1,6 +1,32 @@
import { useUIStateStore } from '@ohif/extension-default';
import LogicalContourOperationsOptions from './components/LogicalContourOperationsOptions';
import SimplifyContourOptions from './components/SimplifyContourOptions';
import SmoothContoursOptions from './components/SmoothContoursOptions';
export function getToolbarModule({ servicesManager }: withAppTypes) {
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
return [
{
name: 'cornerstone.SimplifyContourOptions',
defaultComponent: SimplifyContourOptions,
},
{
name: 'cornerstone.LogicalContourOperationsOptions',
defaultComponent: LogicalContourOperationsOptions,
},
{
name: 'cornerstone.SmoothContoursOptions',
defaultComponent: SmoothContoursOptions,
},
{
name: 'cornerstone.isActiveSegmentationUtility',
evaluate: ({ button }) => {
const { uiState } = useUIStateStore.getState();
return {
isActive: uiState[`activeSegmentationUtility`] === button.id,
};
},
},
{
name: 'evaluate.cornerstone.hasSegmentation',
evaluate: ({ viewportId }) => {
@@ -10,6 +36,30 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
};
},
},
{
name: 'evaluate.cornerstone.hasSegmentationOfType',
evaluate: ({ viewportId, segmentationRepresentationType }) => {
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
if (!segmentations?.length) {
return {
disabled: true,
disabledText: 'No segmentations available',
};
}
if (
!segmentations.some(segmentation =>
Boolean(segmentation.type === segmentationRepresentationType)
)
) {
return {
disabled: true,
disabledText: `No ${segmentationRepresentationType} segmentations available`,
};
}
},
},
{
name: 'evaluate.cornerstone.segmentation',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@@ -3,6 +3,461 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
### Features
* **MeasurementService:** add rendering of unmapped measurements ([#5416](https://github.com/OHIF/Viewers/issues/5416)) ([851e74d](https://github.com/OHIF/Viewers/commit/851e74d7b867a806befb5d85fd71ff9a75e9f2d2))
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"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.12.0-beta.23",
"@ohif/extension-cornerstone": "3.12.0-beta.23",
"@ohif/extension-measurement-tracking": "3.12.0-beta.23",
"@ohif/ui": "3.12.0-beta.23",
"@ohif/core": "3.12.0-beta.78",
"@ohif/extension-cornerstone": "3.12.0-beta.78",
"@ohif/extension-measurement-tracking": "3.12.0-beta.78",
"@ohif/ui": "3.12.0-beta.78",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.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": "^4.0.0",
"@cornerstonejs/core": "^4.0.0",
"@cornerstonejs/tools": "^4.0.0",
"classnames": "^2.3.2"
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.20",
"@cornerstonejs/core": "4.5.20",
"@cornerstonejs/tools": "4.5.20",
"classnames": "2.5.1"
}
}
@@ -22,7 +22,7 @@ const {
CodeScheme: Cornerstone3DCodeScheme,
} = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata;
type InstanceMetadata = OhifTypes.InstanceMetadata;
/**
* TODO
@@ -69,9 +69,10 @@ function addInstances(instances: InstanceMetadata[], _displaySetService: Display
* 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,
@@ -143,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
) {
@@ -227,17 +228,9 @@ function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
);
}
/**
* Checks if measurements can be added to a display set.
*
* @param srDisplaySet - The source display set containing measurements.
* @param newDisplaySet - The new display set to check if measurements can be added.
* @param dataSource - The data source used to retrieve image IDs.
* @param servicesManager - The services manager.
*/
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
srDisplaySet: OhifTypes.DisplaySet,
newDisplaySet: OhifTypes.DisplaySet,
dataSource,
servicesManager: AppTypes.ServicesManager
) {
@@ -289,9 +282,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
is3DMeasurement &&
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
) {
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
addSRAnnotation(measurement, null, null);
addSRAnnotation({ measurement, displaySet: newDisplaySet });
measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
@@ -312,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);
}
}
@@ -328,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;
@@ -363,10 +351,8 @@ 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(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params;
@@ -390,8 +376,8 @@ function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams)
/**
* 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(
@@ -429,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 = {};
@@ -477,8 +463,8 @@ 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 => isScoordOr3d(group) && !isTextPosition(group))) {
@@ -493,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,
@@ -567,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');
@@ -668,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;
@@ -683,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' }
@@ -704,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(
@@ -752,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) {
+3 -3
View File
@@ -9,10 +9,10 @@ import {
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/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';
@@ -28,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);
@@ -7,8 +7,53 @@ import toolNames from '../tools/toolNames';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
export default function addSRAnnotation(measurement, imageId, frameNumber) {
/**
* 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 }));
@@ -52,10 +97,15 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
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,
@@ -64,6 +114,7 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
graphicType,
FrameOfReferenceUID: frameOfReferenceUID,
referencedImageId: imageId,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
},
data: {
label: measurement.labels?.[0]?.value || undefined,
@@ -81,8 +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);
}
@@ -74,17 +74,13 @@ 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;
}
});
@@ -133,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);
@@ -175,9 +169,7 @@ export default function hydrateStructuredReport(
// 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 imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (!imageId) {
return getReferenceData3D(toolData, servicesManager);
@@ -3,6 +3,458 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"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.12.0-beta.23",
"@ohif/extension-cornerstone": "3.12.0-beta.23",
"@ohif/extension-default": "3.12.0-beta.23",
"@ohif/i18n": "3.12.0-beta.23",
"@ohif/ui": "3.12.0-beta.23",
"@ohif/core": "3.12.0-beta.78",
"@ohif/extension-cornerstone": "3.12.0-beta.78",
"@ohif/extension-default": "3.12.0-beta.78",
"@ohif/i18n": "3.12.0-beta.78",
"@ohif/ui": "3.12.0-beta.78",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.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": "^4.0.0",
"@cornerstonejs/tools": "^4.0.0",
"classnames": "^2.3.2"
"@babel/runtime": "7.28.2",
"@cornerstonejs/core": "4.5.20",
"@cornerstonejs/tools": "4.5.20",
"classnames": "2.5.1"
}
}
+485
View File
@@ -3,6 +3,491 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.12.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.77...v3.12.0-beta.78) (2025-10-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.76...v3.12.0-beta.77) (2025-10-28)
### Bug Fixes
* **3DSegmentation:** [Bug] The viewports become blank when loading the seg file in advanced layout after closing the seg file from any other advanced layout ([#5505](https://github.com/OHIF/Viewers/issues/5505)) ([76f7d4e](https://github.com/OHIF/Viewers/commit/76f7d4e23aa359a3397b10adf0bf09df3763f190))
# [3.12.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.75...v3.12.0-beta.76) (2025-10-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.74...v3.12.0-beta.75) (2025-10-24)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.73...v3.12.0-beta.74) (2025-10-23)
### Features
* **jpeg2000:** Add 16-bit RGB support to JPEG2000 decoder ([#5519](https://github.com/OHIF/Viewers/issues/5519)) ([a154443](https://github.com/OHIF/Viewers/commit/a1544432db42bf77864bb7df2f757cae819a4b4d))
# [3.12.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.72...v3.12.0-beta.73) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.71...v3.12.0-beta.72) (2025-10-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.70...v3.12.0-beta.71) (2025-10-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.69...v3.12.0-beta.70) (2025-10-20)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.68...v3.12.0-beta.69) (2025-10-20)
### Bug Fixes
* Pass the correct sop uid + frame for rehydration to prevent associating rehydrated measurements with the wrong data ([#5506](https://github.com/OHIF/Viewers/issues/5506)) ([5037802](https://github.com/OHIF/Viewers/commit/503780247ce7228c602a539fd88ea374e7d0d42f)), closes [#2404](https://github.com/OHIF/Viewers/issues/2404)
# [3.12.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.67...v3.12.0-beta.68) (2025-10-18)
### Bug Fixes
* **segmentation:** Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. ([#5503](https://github.com/OHIF/Viewers/issues/5503)) ([170e860](https://github.com/OHIF/Viewers/commit/170e860aa1b786ab6056cf31076804302a2bc462))
# [3.12.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.66...v3.12.0-beta.67) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.65...v3.12.0-beta.66) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.64...v3.12.0-beta.65) (2025-10-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.63...v3.12.0-beta.64) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.62...v3.12.0-beta.63) (2025-10-15)
### Bug Fixes
* Segmentation not displayed and unable to draw segments in volume viewport after converting Labelmap to Surface ([#5488](https://github.com/OHIF/Viewers/issues/5488)) ([faea09e](https://github.com/OHIF/Viewers/commit/faea09e7ef2667aad5cef1efc2a6516677ca2915))
# [3.12.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.61...v3.12.0-beta.62) (2025-10-15)
### Bug Fixes
* **segmentationService:** prevent no representation crash ([#5495](https://github.com/OHIF/Viewers/issues/5495)) ([3137aed](https://github.com/OHIF/Viewers/commit/3137aed0170196a61e16c6dac0c15bcc41691919))
# [3.12.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.60...v3.12.0-beta.61) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.59...v3.12.0-beta.60) (2025-10-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.58...v3.12.0-beta.59) (2025-10-14)
### Bug Fixes
* **security:** Use exact versioning for dependencies in package.json files. ([#5494](https://github.com/OHIF/Viewers/issues/5494)) ([c7d2017](https://github.com/OHIF/Viewers/commit/c7d2017f081c5803a70bab6c75bba0c975028125))
# [3.12.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.57...v3.12.0-beta.58) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.56...v3.12.0-beta.57) (2025-10-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.55...v3.12.0-beta.56) (2025-10-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.54...v3.12.0-beta.55) (2025-10-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.53...v3.12.0-beta.54) (2025-10-08)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.52...v3.12.0-beta.53) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.51...v3.12.0-beta.52) (2025-10-07)
### Bug Fixes
* **segmentation:** Segmentation not displayed and unable to draw segments when switching back to volume/stack viewport from 3D viewport ([#5430](https://github.com/OHIF/Viewers/issues/5430)) ([78df8ff](https://github.com/OHIF/Viewers/commit/78df8ff03a371904a390c63a95628d5511dad136))
# [3.12.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.50...v3.12.0-beta.51) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.49...v3.12.0-beta.50) (2025-10-07)
### Bug Fixes
* **Segmentation:** [Bug [#5420](https://github.com/OHIF/Viewers/issues/5420)] Segmentation color resets after using toggleOneUp ([#5465](https://github.com/OHIF/Viewers/issues/5465)) ([2c6ce21](https://github.com/OHIF/Viewers/commit/2c6ce21c8f79f8b3e6dc47b5aa6f717b6b5cbd2a))
# [3.12.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.48...v3.12.0-beta.49) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.47...v3.12.0-beta.48) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.46...v3.12.0-beta.47) (2025-10-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.45...v3.12.0-beta.46) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.44...v3.12.0-beta.45) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.43...v3.12.0-beta.44) (2025-10-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.42...v3.12.0-beta.43) (2025-10-06)
### Features
* **MeasurementService:** add rendering of unmapped measurements ([#5416](https://github.com/OHIF/Viewers/issues/5416)) ([851e74d](https://github.com/OHIF/Viewers/commit/851e74d7b867a806befb5d85fd71ff9a75e9f2d2))
# [3.12.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.41...v3.12.0-beta.42) (2025-10-03)
### Bug Fixes
* duplicate segmentation entries on layout switch ([#5458](https://github.com/OHIF/Viewers/issues/5458)) ([9e951e8](https://github.com/OHIF/Viewers/commit/9e951e88005af505ae0c07fbcd3366de12b773dd))
# [3.12.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.40...v3.12.0-beta.41) (2025-10-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.39...v3.12.0-beta.40) (2025-10-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.38...v3.12.0-beta.39) (2025-10-01)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.37...v3.12.0-beta.38) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.36...v3.12.0-beta.37) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.35...v3.12.0-beta.36) (2025-09-30)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.34...v3.12.0-beta.35) (2025-09-29)
### Bug Fixes
* **segmentation:** Restored segment label tool in longitudinal mode. ([#5443](https://github.com/OHIF/Viewers/issues/5443)) ([dd86a8e](https://github.com/OHIF/Viewers/commit/dd86a8eabd0712d25e7652a242377fe09597e8d5))
# [3.12.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.33...v3.12.0-beta.34) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.32...v3.12.0-beta.33) (2025-09-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.31...v3.12.0-beta.32) (2025-09-26)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.30...v3.12.0-beta.31) (2025-09-25)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.29...v3.12.0-beta.30) (2025-09-25)
### Bug Fixes
* **i18n:** Add and update i18n Translation for Toolbar Tool Names ([#5392](https://github.com/OHIF/Viewers/issues/5392)) ([7783d0f](https://github.com/OHIF/Viewers/commit/7783d0f558bbb7016c29513b6fe76ed069dbd75c))
# [3.12.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.28...v3.12.0-beta.29) (2025-09-22)
### Features
* **Segmentation:** Segmentation highlight animation function selection ([#5401](https://github.com/OHIF/Viewers/issues/5401)) ([69dbe27](https://github.com/OHIF/Viewers/commit/69dbe2778493cdfc7a8da5e757c95217bb31edc2))
# [3.12.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.27...v3.12.0-beta.28) (2025-09-19)
### Bug Fixes
* **SegmentationService:** _getSegmentCenter wrongly handling seg center availability ([#5396](https://github.com/OHIF/Viewers/issues/5396)) ([74b82bd](https://github.com/OHIF/Viewers/commit/74b82bd93442411ad68f4872992126c40321eb9e))
# [3.12.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.26...v3.12.0-beta.27) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.25...v3.12.0-beta.26) (2025-09-17)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.24...v3.12.0-beta.25) (2025-09-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.23...v3.12.0-beta.24) (2025-09-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.12.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.12.0-beta.22...v3.12.0-beta.23) (2025-09-05)
+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`
+32 -30
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "3.12.0-beta.23",
"version": "3.12.0-beta.78",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -31,41 +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": "^4.0.0",
"@ohif/core": "3.12.0-beta.23",
"@ohif/ui": "3.12.0-beta.23",
"@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.20",
"@ohif/core": "3.12.0-beta.78",
"@ohif/ui": "3.12.0-beta.78",
"dcmjs": "0.43.1",
"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"
"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": "^4.0.0",
"@cornerstonejs/ai": "^4.0.0",
"@cornerstonejs/core": "^4.0.0",
"@cornerstonejs/labelmap-interpolation": "^4.0.0",
"@cornerstonejs/polymorphic-segmentation": "^4.0.0",
"@cornerstonejs/tools": "^4.0.0",
"@babel/runtime": "7.28.2",
"@cornerstonejs/adapters": "4.5.20",
"@cornerstonejs/ai": "4.5.20",
"@cornerstonejs/core": "4.5.20",
"@cornerstonejs/labelmap-interpolation": "4.5.20",
"@cornerstonejs/polymorphic-segmentation": "4.5.20",
"@cornerstonejs/tools": "4.5.20",
"@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"
}
}
+348 -76
View File
@@ -14,13 +14,18 @@ import {
utilities as cstUtils,
annotation,
Types as ToolTypes,
SplineContourSegmentationTool,
} from '@cornerstonejs/tools';
import {
SegmentInfo,
LogicalOperation,
OperatorOptions,
} from '@cornerstonejs/tools/utilities/contourSegmentation/logicalOperators';
import * as cornerstoneTools from '@cornerstonejs/tools';
import * as labelmapInterpolation from '@cornerstonejs/labelmap-interpolation';
import { ONNXSegmentationController } from '@cornerstonejs/ai';
import { Types as OhifTypes, utils } from '@ohif/core';
import i18n from '@ohif/i18n';
import {
callInputDialogAutoComplete,
createReportAsync,
@@ -33,7 +38,11 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import {
usePositionPresentationStore,
useSegmentationPresentationStore,
useSelectedSegmentationsForViewportStore,
} from './stores';
import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
@@ -42,6 +51,12 @@ import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { isMeasurementWithinViewport } from './utils/isMeasurementWithinViewport';
import { getCenterExtent } from './utils/getCenterExtent';
import { EasingFunctionEnum } from './utils/transitions';
import { createSegmentationForViewport } from './utils/createSegmentationForViewport';
import { utilities as segmentationUtilities } from '@cornerstonejs/tools/segmentation';
import i18n from '@ohif/i18n';
const { add, intersect, subtract, copy } = cstUtils.contourSegmentation;
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
@@ -235,16 +250,28 @@ function commandsModule({
return;
}
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
if (displaySet.isOverlayDisplaySet) {
// 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
const segmentationType =
// Todo: check if PMAP modality should be handled such as SEG
displaySet.Modality !== 'SEG'
? SegmentationRepresentations.Contour
: viewport.type === CoreEnums.ViewportType.VOLUME_3D
? SegmentationRepresentations.Surface
: SegmentationRepresentations.Labelmap;
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet,
type:
displaySet.Modality === 'SEG'
? SegmentationRepresentations.Labelmap
: SegmentationRepresentations.Contour,
type: segmentationType,
});
}
@@ -265,10 +292,35 @@ function commandsModule({
referencedDisplaySetInstanceUID
);
storePositionPresentation(referencedDisplaySet);
return commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
const results = commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
const disableEditing = customizationService.getCustomization(
'panelSegmentation.disableEditing'
);
if (disableEditing) {
const segmentationRepresentations = segmentationService.getSegmentationRepresentations(
viewportId,
{
segmentationId: displaySet.displaySetInstanceUID,
}
);
segmentationRepresentations.forEach(representation => {
const segmentIndices = Object.keys(representation.segments);
segmentIndices.forEach(segmentIndex => {
segmentationService.setSegmentLocked(
representation.segmentationId,
parseInt(segmentIndex),
true
);
});
});
}
return results;
} else if (displaySet.Modality === 'SR') {
const results = commandsManager.runCommand('hydrateStructuredReport', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
@@ -517,8 +569,8 @@ function commandsModule({
if (!labelConfig) {
const label = await callInputDialog({
uiDialogService,
title: 'Edit Measurement Label',
placeholder: measurement.label || 'Enter new label',
title: i18n.t('Tools:Edit Measurement Label'),
placeholder: measurement.label || i18n.t('Tools:Enter new label'),
defaultValue: measurement.label,
});
@@ -732,8 +784,8 @@ function commandsModule({
if (!labelConfig) {
const label = await callInputDialog({
uiDialogService,
title: 'Edit Arrow Text',
placeholder: data?.data?.label || 'Enter new text',
title: i18n.t('Tools:Edit Arrow Text'),
placeholder: data?.data?.label || i18n.t('Tools:Enter new text'),
defaultValue: data?.data?.label || '',
});
@@ -925,17 +977,21 @@ function commandsModule({
}
}
},
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [] }) => {
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [], bindings }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
toolName = toolName || itemId || value;
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
toolGroupIds.forEach(toolGroupId => {
actions.setToolActive({ toolName, toolGroupId });
actions.setToolActive({ toolName, toolGroupId, bindings });
});
},
setToolActive: ({ toolName, toolGroupId = null }) => {
setToolActive: ({
toolName,
toolGroupId = null,
bindings = [{ mouseButton: Enums.MouseBindings.Primary }],
}) => {
const { viewports } = viewportGridService.getState();
if (!viewports.size) {
@@ -963,11 +1019,7 @@ function commandsModule({
// Set the new toolName to be active
toolGroup.setToolActive(toolName, {
bindings: [
{
mouseButton: Enums.MouseBindings.Primary,
},
],
bindings,
});
},
// capture viewport
@@ -977,8 +1029,8 @@ function commandsModule({
if (!cornerstoneViewportService.getCornerstoneViewport(activeViewportId)) {
// Cannot download a non-cornerstone viewport (image).
uiNotificationService.show({
title: 'Download Image',
message: 'Image cannot be downloaded',
title: i18n.t('Tools:Download Image'),
message: i18n.t('Tools:Image cannot be downloaded'),
type: 'error',
});
return;
@@ -989,7 +1041,7 @@ function commandsModule({
if (uiModalService) {
uiModalService.show({
content: CornerstoneViewportDownloadForm,
title: 'Download High Quality Image',
title: i18n.t('Tools:Download High Quality Image'),
contentProps: {
activeViewportId,
cornerstoneViewportService,
@@ -1440,48 +1492,24 @@ function commandsModule({
* as a segmentation representation to the viewport.
*/
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
const { viewportGridService, displaySetService, segmentationService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID =
options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
const segs = segmentationService.getSegmentations();
const label = options.label || `Segmentation ${segs.length + 1}`;
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// This will create the segmentation and register it as a display set
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
displaySet,
{
label,
segmentationId,
segments: options.createInitialSegment
? {
1: {
label: `${i18n.t('Segment')} 1`,
active: true,
},
}
: {},
}
);
// Also add the segmentation representation to the viewport
await segmentationService.addSegmentationRepresentation(viewportId, {
segmentationId,
type: Enums.SegmentationRepresentations.Labelmap,
return createSegmentationForViewport(servicesManager, {
viewportId,
options,
segmentationType: SegmentationRepresentations.Labelmap,
});
},
/**
* Creates a contour for the active viewport
*
* The created contour will be registered as a display set and also added
* as a segmentation representation to the viewport.
*/
createContourForViewport: async ({ viewportId, options = {} }) => {
return createSegmentationForViewport(servicesManager, {
viewportId,
options,
segmentationType: SegmentationRepresentations.Contour,
});
return generatedSegmentationId;
},
/**
@@ -1500,9 +1528,9 @@ function commandsModule({
* Adds a new segment to a segmentation
* @param props.segmentationId - The ID of the segmentation to add the segment to
*/
addSegmentCommand: ({ segmentationId }) => {
addSegmentCommand: ({ segmentationId, config }) => {
const { segmentationService } = servicesManager.services;
segmentationService.addSegment(segmentationId);
segmentationService.addSegment(segmentationId, config);
},
/**
@@ -1518,7 +1546,33 @@ function commandsModule({
segmentationId
);
segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
const { highlightAlpha, highlightSegment, animationLength, animationFunctionType } =
(customizationService.getCustomization(
'panelSegmentation.jumpToSegmentHighlightAnimationConfig'
) as Object as {
highlightAlpha?: number;
highlightSegment?: boolean;
animationLength?: number;
animationFunctionType?: EasingFunctionEnum;
}) ?? {};
const validAnimationFunctionType = Object.values(EasingFunctionEnum).includes(
animationFunctionType
)
? animationFunctionType
: undefined;
segmentationService.jumpToSegmentCenter(
segmentationId,
segmentIndex,
undefined,
highlightAlpha,
highlightSegment,
animationLength,
undefined,
validAnimationFunctionType
);
},
/**
@@ -1718,8 +1772,8 @@ function commandsModule({
callInputDialog({
uiDialogService,
title: 'Edit Segment Label',
placeholder: 'Enter new label',
title: i18n.t('Tools:Edit Segment Label'),
placeholder: i18n.t('Tools:Enter new label'),
defaultValue: segment.label,
}).then(label => {
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
@@ -1738,8 +1792,8 @@ function commandsModule({
callInputDialog({
uiDialogService,
title: 'Edit Segmentation Label',
placeholder: 'Enter new label',
title: i18n.t('Tools:Edit Segmentation Label'),
placeholder: i18n.t('Tools:Enter new label'),
defaultValue: label,
}).then(label => {
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
@@ -1761,7 +1815,7 @@ function commandsModule({
uiDialogService.show({
content: colorPickerDialog,
title: 'Segment Color',
title: i18n.t('Tools:Segment Color'),
contentProps: {
value: rgbaColor,
onSave: newRgbaColor => {
@@ -1831,22 +1885,24 @@ function commandsModule({
}
});
},
toggleSegmentLabel: () => {
toggleSegmentLabel: ({ enabled }: { enabled?: boolean }) => {
const toolName = cornerstoneTools.SegmentLabelTool.toolName;
const toolGroupIds = toolGroupService.getToolGroupIds();
const isOn = toolGroupIds.some(toolGroupId => {
const isToolOn = toolGroupIds.some(toolGroupId => {
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
const mode = toolGroup.getToolInstance(toolName)?.mode;
return mode === 'Active';
});
const enableTool = enabled !== undefined ? enabled : !isToolOn;
toolGroupIds.forEach(toolGroupId => {
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
if (isOn) {
toolGroup.setToolDisabled(toolName);
} else {
if (enableTool) {
toolGroup.setToolActive(toolName);
} else {
toolGroup.setToolDisabled(toolName);
}
});
},
@@ -2203,6 +2259,207 @@ function commandsModule({
deleting,
});
},
activateSelectedSegmentationOfType: ({ segmentationRepresentationType }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
const activeViewportId = viewportGridService.getActiveViewportId();
const { selectedSegmentationsForViewport } =
useSelectedSegmentationsForViewportStore.getState();
const segmentationId = selectedSegmentationsForViewport[activeViewportId]?.get(
segmentationRepresentationType
);
if (!segmentationId) {
return;
}
segmentationService.setActiveSegmentation(activeViewportId, segmentationId);
},
setDynamicCursorSizeForSculptorTool: ({ value: isDynamicCursorSize }) => {
const viewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const sculptorToolInstance = toolGroup.getToolInstance(toolNames.SculptorTool);
const oldConfiguration = sculptorToolInstance.configuration;
sculptorToolInstance.configuration = {
...oldConfiguration,
updateCursorSize: isDynamicCursorSize ? 'dynamic' : '',
};
},
setInterpolationToolConfiguration: ({ value: interpolateContours, toolNames }) => {
const viewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
// Set the interpolation configuration for the active tool.
const activeTool = toolGroupService.getActiveToolForViewport(viewportId);
const interpolationConfig = {
interpolation: {
enabled: interpolateContours,
showInterpolationPolyline: true,
},
};
toolGroup.setToolConfiguration(activeTool, interpolationConfig);
// Now set the interpolation configuration for the other tools specified.
if (toolNames) {
Object.values(toolGroup.getToolInstances()).forEach(toolInstance => {
if (toolNames?.includes(toolInstance.toolName)) {
toolGroup.setToolConfiguration(toolInstance.toolName, interpolationConfig);
}
});
}
},
setSimplifiedSplineForSplineContourSegmentationTool: ({ value: simplifiedSpline }) => {
const viewportId = viewportGridService.getActiveViewportId();
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
Object.values(toolGroup.getToolInstances()).forEach(toolInstance => {
if (toolInstance instanceof SplineContourSegmentationTool) {
const oldConfiguration = toolInstance.configuration;
toolInstance.configuration = {
...oldConfiguration,
simplifiedSpline,
};
}
});
},
removeSmallContours: ({ areaThreshold: threshold }) => {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const activeSegment = segmentationService.getActiveSegment(viewportId);
if (!activeSegmentation || !activeSegment) {
return;
}
const { removeContourIslands } = segmentationUtilities;
removeContourIslands(activeSegmentation.segmentationId, activeSegment.segmentIndex, {
threshold,
});
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
applyLogicalContourOperation: ({
segmentAInfo,
segmentBInfo,
resultSegmentInfo,
logicalOperation,
}: {
segmentAInfo: SegmentInfo;
segmentBInfo: SegmentInfo;
resultSegmentInfo: OperatorOptions;
logicalOperation: LogicalOperation;
}) => {
switch (logicalOperation) {
case LogicalOperation.Union:
add(segmentAInfo, segmentBInfo, resultSegmentInfo);
break;
case LogicalOperation.Intersect:
intersect(segmentAInfo, segmentBInfo, resultSegmentInfo);
break;
case LogicalOperation.Subtract:
subtract(segmentAInfo, segmentBInfo, resultSegmentInfo);
break;
default:
throw new Error('Unsupported logical operation');
break;
}
},
copyContourSegment: ({
sourceSegmentInfo,
targetSegmentInfo,
}: {
sourceSegmentInfo: SegmentInfo;
targetSegmentInfo?: SegmentInfo;
}) => {
if (!targetSegmentInfo) {
targetSegmentInfo = {
segmentationId: sourceSegmentInfo.segmentationId,
segmentIndex: segmentationService.getNextAvailableSegmentIndex(
sourceSegmentInfo.segmentationId
),
};
segmentationService.addSegment(targetSegmentInfo.segmentationId, {
segmentIndex: targetSegmentInfo.segmentIndex,
});
}
copy(sourceSegmentInfo, targetSegmentInfo);
},
smoothContours: () => {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const activeSegment = segmentationService.getActiveSegment(viewportId);
if (!activeSegmentation || !activeSegment) {
return;
}
const { smoothContours } = segmentationUtilities;
smoothContours(activeSegmentation.segmentationId, activeSegment.segmentIndex);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
removeContourHoles: () => {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const activeSegment = segmentationService.getActiveSegment(viewportId);
if (!activeSegmentation || !activeSegment) {
return;
}
const { removeContourHoles } = segmentationUtilities;
removeContourHoles(activeSegmentation.segmentationId, activeSegment.segmentIndex);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
decimateContours: () => {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const activeSegment = segmentationService.getActiveSegment(viewportId);
if (!activeSegmentation || !activeSegment) {
return;
}
const { decimateContours } = segmentationUtilities;
decimateContours(activeSegmentation.segmentationId, activeSegment.segmentIndex);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
convertContourHoles: () => {
const viewportId = viewportGridService.getActiveViewportId();
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
const activeSegment = segmentationService.getActiveSegment(viewportId);
if (!activeSegmentation || !activeSegment) {
return;
}
const targetSegmentInfo = {
segmentationId: activeSegmentation.segmentationId,
segmentIndex: segmentationService.getNextAvailableSegmentIndex(
activeSegmentation.segmentationId
),
};
segmentationService.addSegment(targetSegmentInfo.segmentationId, {
segmentIndex: targetSegmentInfo.segmentIndex,
});
const { convertContourHoles } = segmentationUtilities;
convertContourHoles(
activeSegmentation.segmentationId,
activeSegment.segmentIndex,
targetSegmentInfo.segmentationId,
targetSegmentInfo.segmentIndex
);
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
renderingEngine.render();
},
};
const definitions = {
@@ -2401,6 +2658,9 @@ function commandsModule({
createLabelmapForViewport: {
commandFn: actions.createLabelmapForViewport,
},
createContourForViewport: {
commandFn: actions.createContourForViewport,
},
setActiveSegmentation: {
commandFn: actions.setActiveSegmentation,
},
@@ -2502,6 +2762,18 @@ function commandsModule({
toggleSegmentLabel: actions.toggleSegmentLabel,
jumpToMeasurementViewport: actions.jumpToMeasurementViewport,
initializeSegmentLabelTool: actions.initializeSegmentLabelTool,
activateSelectedSegmentationOfType: actions.activateSelectedSegmentationOfType,
setDynamicCursorSizeForSculptorTool: actions.setDynamicCursorSizeForSculptorTool,
setSimplifiedSplineForSplineContourSegmentationTool:
actions.setSimplifiedSplineForSplineContourSegmentationTool,
removeSmallContours: actions.removeSmallContours,
applyLogicalContourOperation: actions.applyLogicalContourOperation,
copyContourSegment: actions.copyContourSegment,
smoothContours: actions.smoothContours,
removeContourHoles: actions.removeContourHoles,
decimateContours: actions.decimateContours,
convertContourHoles: actions.convertContourHoles,
setInterpolationToolConfiguration: actions.setInterpolationToolConfiguration,
};
return {
@@ -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);
@@ -0,0 +1,111 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuPortal,
DropdownMenuSubContent,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuSeparator,
Icons,
} from '@ohif/ui-next';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
interface ExportSegmentationSubMenuItemProps {
segmentationId: string;
segmentationRepresentationType: string;
allowExport: boolean;
actions: {
storeSegmentation: (segmentationId: string) => Promise<void>;
onSegmentationDownloadRTSS: (segmentationId: string) => void;
onSegmentationDownload: (segmentationId: string) => void;
downloadCSVSegmentationReport: (segmentationId: string) => void;
};
}
export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuItemProps> = ({
segmentationId,
segmentationRepresentationType,
allowExport,
actions,
}) => {
const { t } = useTranslation('SegmentationTable');
return (
<>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download & Export')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.downloadCSVSegmentationReport(segmentationId);
}}
disabled={!allowExport}
>
{t('CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM RTSS')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)}
{segmentationRepresentationType === SegmentationRepresentations.Contour && (
<>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download DICOM RTSS')}</span>
</DropdownMenuItem>
</>
)}
</>
);
};
@@ -0,0 +1,69 @@
import React, { useState } from 'react';
import { Switch } from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
export default function SegmentationToolConfig() {
const { commandsManager } = useSystem();
// Get initial states based on current configuration
const [previewEdits, setPreviewEdits] = useState(false);
const [segmentLabelEnabled, setSegmentLabelEnabled] = useState(false);
const [toggleSegmentEnabled, setToggleSegmentEnabled] = useState(false);
const [useCenterAsSegmentIndex, setUseCenterAsSegmentIndex] = useState(false);
const handlePreviewEditsChange = checked => {
setPreviewEdits(checked);
commandsManager.run('toggleSegmentPreviewEdit', { toggle: checked });
};
const handleToggleSegmentEnabledChange = checked => {
setToggleSegmentEnabled(checked);
commandsManager.run('toggleSegmentSelect', { toggle: checked });
};
const handleUseCenterAsSegmentIndexChange = checked => {
setUseCenterAsSegmentIndex(checked);
commandsManager.run('toggleUseCenterSegmentIndex', { toggle: checked });
};
const handleSegmentLabelEnabledChange = checked => {
setSegmentLabelEnabled(checked);
commandsManager.run('toggleSegmentLabel', { enabled: checked });
};
return (
<div className="bg-muted flex flex-col gap-2 border-b border-b-[2px] border-black px-2 py-3">
<div className="flex items-center gap-2">
<Switch
checked={previewEdits}
onCheckedChange={handlePreviewEditsChange}
/>
<span className="text-foreground text-base">Preview edits before creating</span>
</div>
<div className="flex items-center gap-2">
<Switch
checked={useCenterAsSegmentIndex}
onCheckedChange={handleUseCenterAsSegmentIndexChange}
/>
<span className="text-foreground text-base">Use center as segment index</span>
</div>
<div className="flex items-center gap-2">
<Switch
checked={toggleSegmentEnabled}
onCheckedChange={handleToggleSegmentEnabledChange}
/>
<span className="text-foreground text-base">Hover on segment border to activate</span>
</div>
<div className="flex items-center gap-2">
<Switch
checked={segmentLabelEnabled}
onCheckedChange={handleSegmentLabelEnabledChange}
/>
<span className="text-foreground text-base">Show segment name on hover</span>
</div>
</div>
);
}
@@ -0,0 +1,64 @@
import React, { useCallback } from 'react';
import { cn, ToolButton } from '@ohif/ui-next';
import { useUIStateStore } from '@ohif/extension-default';
interface SegmentationUtilityButtonProps {
className?: string;
isActive?: boolean;
id: string;
}
/**
* A button that represents a segmentation utility.
* It is implicitly the PopoverTrigger for the options Popover panel that is
* opened in the PanelSegmentation component. It in essence is the PopoverTrigger
* because typically this button has the associated options to display in the Popover.
* Furthermore it also typically has a command for toggling the activeSegmentationUtility
* value in the UIStateStore that triggers the fetching of its options.
* @param props - The props for the button.
* @param props.className - The class name for the button.
* @param props.isActive - Whether the button is active
* @param props.id - The id of the button.
*/
function SegmentationUtilityButton(props: SegmentationUtilityButtonProps) {
const { className, isActive, id } = props;
const activeSegmentationUtility = useUIStateStore(
store => store.uiState.activeSegmentationUtility
);
const toolButtonClassName = cn(
'w-7 h-7 text-primary hover:text-primary hover:!bg-primary/30',
className,
isActive && 'bg-primary/30'
);
const handleMouseDownCapture = useCallback(
event => {
if (activeSegmentationUtility === id) {
// If this active button is clicked, prevent the default Popover
// behaviour of closing the Popover on a pointer/mouse down.
// Not doing this will cause the Popover to close and then reopen again.
// Why? Because propagating this event will cause PanelSegmentation to
// close the Popover by clearing the activeSegmentationUtility. Then
// this button will set the activeSegmentationUtility again and the
// Popover will reopen.
event.preventDefault();
event.stopPropagation();
}
},
[activeSegmentationUtility, id]
);
return (
<div onPointerDownCapture={handleMouseDownCapture}>
<ToolButton
{...props}
className={toolButtonClassName}
/>
</div>
);
}
export default SegmentationUtilityButton;
@@ -1,6 +1,5 @@
import React from 'react';
import { useActiveViewportDisplaySets, useSystem, utils } from '@ohif/core';
// import { AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
import { useActiveViewportDisplaySets, utils } from '@ohif/core';
import { AccordionGroup } from './AccordionGroup';
import MeasurementsOrAdditionalFindings from './MeasurementsOrAdditionalFindings';
@@ -63,8 +62,7 @@ export const groupByStudy = (items, grouping, childProps) => {
export function StudyMeasurements(props): React.ReactNode {
const { items, grouping = {}, children } = props;
const system = useSystem();
const activeDisplaySets = useActiveViewportDisplaySets(system);
const activeDisplaySets = useActiveViewportDisplaySets();
const activeStudyUID = activeDisplaySets?.[0]?.StudyInstanceUID;
return (
@@ -1,9 +1,11 @@
import React, { ReactElement, useCallback } from 'react';
import { Switch } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { useTranslation } from 'react-i18next';
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const { t } = useTranslation('WindowLevelActionMenu');
const handleToggle = useCallback(() => {
toggleColorbar();
@@ -16,7 +18,7 @@ export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactEle
className="flex-grow"
onClick={handleToggle}
>
Display Color bar
{t('Display Color bar')}
</span>
<Switch
className="ml-2 flex-shrink-0"
@@ -1,9 +1,11 @@
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { useTranslation } from 'react-i18next';
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
const { t } = useTranslation('WindowLevelActionMenu');
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
@@ -74,7 +76,7 @@ export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactEle
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
>
<span className="flex-shrink-0">Preview in viewport</span>
<span className="flex-shrink-0">{t('Preview in viewport')}</span>
<Switch
className="ml-auto flex-shrink-0"
checked={showPreview}
@@ -7,6 +7,7 @@ import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import i18n from 'i18next';
export type WindowLevelActionMenuProps = {
viewportId: string;
@@ -58,6 +59,7 @@ export function WindowLevelActionMenuContent({
isVisible={true}
align={align}
side={side}
backLabel={i18n.t('WindowLevelActionMenu:Back to Display Options')}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && <Colorbar viewportId={viewportId} />}
@@ -65,7 +67,7 @@ export function WindowLevelActionMenuContent({
{colorbarProperties?.colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemLabel={t('Color LUT')}
itemIcon="icon-color-lut"
className="flex h-[calc(100%-32px)] flex-col"
>
@@ -86,7 +88,7 @@ export function WindowLevelActionMenuContent({
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<AllInOneMenu.SubMenu itemLabel={t('Rendering Options')}>
<VolumeRenderingOptions viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
@@ -3,17 +3,14 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
Icons,
useSegmentationTableContext,
useSegmentationExpanded,
} from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
import { useSystem } from '@ohif/core/src';
import { ExportSegmentationSubMenuItem } from '../components/ExportSegmentationSubMenuItem';
/**
* Custom dropdown menu component for segmentation panel that uses context for data
@@ -29,6 +26,8 @@ export const CustomDropdownMenuContent = () => {
exportOptions,
activeSegmentation,
activeSegmentationId,
segmentationRepresentationType,
disableEditing,
} = useSegmentationTableContext('CustomDropdownMenu');
// Try to get segmentation data from expanded context first, fall back to table context
@@ -78,10 +77,14 @@ export const CustomDropdownMenuContent = () => {
return (
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => onSegmentationAdd(segmentationId)}>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
{!disableEditing && (
<DropdownMenuItem
onClick={() => onSegmentationAdd({ segmentationId, segmentationRepresentationType })}
>
<Icons.Add className="text-foreground" />
<span className="pl-2">{t('Create New Segmentation')}</span>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuLabel>{t('Manage Current Segmentation')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => onSegmentationRemoveFromViewport(segmentationId)}>
@@ -92,61 +95,12 @@ export const CustomDropdownMenuContent = () => {
<Icons.Rename className="text-foreground" />
<span className="pl-2">{t('Rename')}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download & Export')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.downloadCSVSegmentationReport(segmentationId);
}}
disabled={!allowExport}
>
{t('CSV Report')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM RTSS')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.storeSegmentation(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<ExportSegmentationSubMenuItem
segmentationId={segmentationId}
segmentationRepresentationType={segmentationRepresentationType}
allowExport={allowExport}
actions={actions}
/>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onSegmentationDelete(segmentationId)}>
<Icons.Delete className="text-red-600" />
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { ImageModal, FooterAction } from '@ohif/ui-next';
import { useTranslation } from 'react-i18next';
const MAX_TEXTURE_SIZE = 10000;
const DEFAULT_FILENAME = 'image';
@@ -37,6 +37,7 @@ function ViewportDownloadFormNew({
const [showWarningMessage, setShowWarningMessage] = useState(true);
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
const { t } = useTranslation('CaptureViewportModal');
useEffect(() => {
if (!viewportElement) {
@@ -82,7 +83,7 @@ function ViewportDownloadFormNew({
value={filename}
onChange={e => setFilename(e.target.value)}
>
File name
{t('File name')}
</ImageModal.Filename>
<ImageModal.Filetype
selected={fileType}
@@ -109,7 +110,7 @@ function ViewportDownloadFormNew({
maxWidth={MAX_TEXTURE_SIZE.toString()}
maxHeight={MAX_TEXTURE_SIZE.toString()}
>
Image size <span className="text-muted-foreground">px</span>
{t('Image size')} <span className="text-muted-foreground">px</span>
</ImageModal.ImageSize>
<ImageModal.SwitchOption
@@ -117,7 +118,7 @@ function ViewportDownloadFormNew({
checked={showAnnotations}
onCheckedChange={onAnnotationsChange}
>
Include annotations
{t('Include annotations')}
</ImageModal.SwitchOption>
{warningState.enabled && (
<ImageModal.SwitchOption
@@ -125,19 +126,21 @@ function ViewportDownloadFormNew({
checked={showWarningMessage}
onCheckedChange={setShowWarningMessage}
>
Include warning message
{t('Include warning message')}
</ImageModal.SwitchOption>
)}
<FooterAction className="mt-2">
<FooterAction.Right>
<FooterAction.Secondary onClick={onClose}>Cancel</FooterAction.Secondary>
<FooterAction.Secondary onClick={onClose}>
{t('Common:Cancel')}
</FooterAction.Secondary>
<FooterAction.Primary
onClick={() => {
onDownload(filename || DEFAULT_FILENAME, fileType);
onClose();
}}
>
Save
{t('Common:Save')}
</FooterAction.Primary>
</FooterAction.Right>
</FooterAction>
@@ -1,7 +1,9 @@
import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
import React, { useState } from 'react';
import { Switch } from '@ohif/ui-next';
import SegmentationToolConfig from '../components/SegmentationToolConfig';
import React from 'react';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import * as cornerstoneTools from '@cornerstonejs/tools';
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
return {
@@ -9,10 +11,25 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
'panelSegmentation.customSegmentStatisticsHeader': CustomSegmentStatisticsHeader,
'panelSegmentation.disableEditing': false,
'panelSegmentation.showAddSegment': true,
'panelSegmentation.onSegmentationAdd': () => {
'panelSegmentation.onSegmentationAdd': async ({
segmentationRepresentationType = SegmentationRepresentations.Labelmap,
}) => {
const { viewportGridService } = servicesManager.services;
const viewportId = viewportGridService.getState().activeViewportId;
commandsManager.run('createLabelmapForViewport', { viewportId });
if (segmentationRepresentationType === SegmentationRepresentations.Labelmap) {
commandsManager.run('createLabelmapForViewport', { viewportId });
} else if (segmentationRepresentationType === SegmentationRepresentations.Contour) {
const segmentationId = await commandsManager.run('createContourForViewport', {
viewportId,
});
cornerstoneTools.segmentation.config.style.setStyle(
{ segmentationId, type: SegmentationRepresentations.Contour },
{
fillAlpha: 0.5,
renderFill: true,
}
);
}
},
'panelSegmentation.tableMode': 'collapsed',
'panelSegmentation.readableText': {
@@ -33,53 +50,11 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
lesionGlycolysis: 'Lesion Glycolysis',
center: 'Center',
},
'segmentationToolbox.config': () => {
// Get initial states based on current configuration
const [previewEdits, setPreviewEdits] = useState(false);
const [toggleSegmentEnabled, setToggleSegmentEnabled] = useState(false);
const [useCenterAsSegmentIndex, setUseCenterAsSegmentIndex] = useState(false);
const handlePreviewEditsChange = checked => {
setPreviewEdits(checked);
commandsManager.run('toggleSegmentPreviewEdit', { toggle: checked });
};
const handleToggleSegmentEnabledChange = checked => {
setToggleSegmentEnabled(checked);
commandsManager.run('toggleSegmentSelect', { toggle: checked });
};
const handleUseCenterAsSegmentIndexChange = checked => {
setUseCenterAsSegmentIndex(checked);
commandsManager.run('toggleUseCenterSegmentIndex', { toggle: checked });
};
return (
<div className="bg-muted flex flex-col gap-4 border-b border-b-[2px] border-black px-2 py-3">
<div className="flex items-center gap-2">
<Switch
checked={previewEdits}
onCheckedChange={handlePreviewEditsChange}
/>
<span className="text-base text-white">Preview edits before creating</span>
</div>
<div className="flex items-center gap-2">
<Switch
checked={useCenterAsSegmentIndex}
onCheckedChange={handleUseCenterAsSegmentIndexChange}
/>
<span className="text-base text-white">Use center as segment index</span>
</div>
<div className="flex items-center gap-2">
<Switch
checked={toggleSegmentEnabled}
onCheckedChange={handleToggleSegmentEnabledChange}
/>
<span className="text-base text-white">Hover on segment border to activate</span>
</div>
</div>
);
'labelMapSegmentationToolbox.config': () => {
return <SegmentationToolConfig />;
},
'contourSegmentationToolbox.config': () => {
return <SegmentationToolConfig />;
},
};
}
+42 -13
View File
@@ -1,53 +1,67 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Toolbox } from '@ohif/extension-default';
import PanelSegmentation from './panels/PanelSegmentation';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
import PanelMeasurement from './panels/PanelMeasurement';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import i18n from '@ohif/i18n';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
const wrappedPanelSegmentation = ({ configuration }) => {
const { toolbarService } = servicesManager.services;
const toolSectionMap = {
[SegmentationRepresentations.Labelmap]: toolbarService.sections.labelMapSegmentationToolbox,
[SegmentationRepresentations.Contour]: toolbarService.sections.contourSegmentationToolbox,
};
const wrappedPanelSegmentation = props => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
...props?.configuration,
}}
segmentationRepresentationType={props?.segmentationRepresentationType}
/>
);
};
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
const wrappedPanelSegmentationNoHeader = props => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
...props?.configuration,
}}
segmentationRepresentationType={props?.segmentationRepresentationType}
/>
);
};
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
const { toolbarService } = servicesManager.services;
const wrappedPanelSegmentationWithTools = props => {
const { t } = useTranslation('SegmentationTable');
const tKey = `${props.segmentationRepresentationType ?? 'Segmentation'} tools`;
const tValue = t(tKey);
return (
<>
<Toolbox
buttonSectionId={toolbarService.sections.segmentationToolbox}
title="Segmentation Tools"
buttonSectionId={toolSectionMap[props.segmentationRepresentationType]}
title={tValue}
/>
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
...props?.configuration,
}}
segmentationRepresentationType={props?.segmentationRepresentationType}
/>
</>
);
@@ -82,11 +96,26 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
component: wrappedPanelSegmentationNoHeader,
},
{
name: 'panelSegmentationWithTools',
name: 'panelSegmentationWithToolsLabelMap',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationWithTools,
label: i18n.t('SegmentationTable:Labelmap'),
component: props =>
wrappedPanelSegmentationWithTools({
...props,
segmentationRepresentationType: SegmentationRepresentations.Labelmap,
}),
},
{
name: 'panelSegmentationWithToolsContour',
iconName: 'tab-contours',
iconLabel: 'Segmentation',
label: i18n.t('SegmentationTable:Contour'),
component: props =>
wrappedPanelSegmentationWithTools({
...props,
segmentationRepresentationType: SegmentationRepresentations.Contour,
}),
},
];
};
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { useSystem } from '@ohif/core';
function mapMeasurementToDisplay(measurement, displaySetService) {
const { referenceSeriesUID } = measurement;
@@ -682,7 +682,7 @@ export function useViewportRendering(
const actorEntries = viewport.getActors();
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(activeDisplaySetInstanceUID)
entry.referencedId?.includes(activeDisplaySetInstanceUID)
);
if (!actorEntry) {
@@ -152,15 +152,27 @@ export function useViewportSegmentations({
const representations = segmentationService.getSegmentationRepresentations(viewportId);
const newSegmentationsWithRepresentations = representations.map(representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
// Deduplicate representations by segmentationId to prevent showing
// the same segmentation multiple times in the panel when it has
// multiple representation types (e.g., labelmap and surface)
const uniqueSegmentationMap = new Map();
representations.forEach(representation => {
if (!uniqueSegmentationMap.has(representation.segmentationId)) {
uniqueSegmentationMap.set(representation.segmentationId, representation);
}
});
const newSegmentationsWithRepresentations = Array.from(uniqueSegmentationMap.values()).map(
representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
}
);
setSegmentationsWithRepresentations({
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
disabled: false,
+2 -2
View File
@@ -1,9 +1,9 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const fourUp = {
id: 'fourUp',
locked: true,
name: '3D four up',
name: i18n.t('Hps:3D four up'),
icon: 'layout-advanced-3d-four-up',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
+3 -3
View File
@@ -1,9 +1,9 @@
import { Types } from '@ohif/core';
import i18n from 'i18next';
const frameView: Types.HangingProtocol.Protocol = {
id: '@ohif/frameView',
description: 'Frame view for the active series',
name: 'Frame View',
description: i18n.t('Hps:Frame view for the active series'),
name: i18n.t('Hps:Frame View'),
icon: 'tool-stack-scroll',
isPreset: true,
toolGroupIds: ['default'],
+2 -2
View File
@@ -1,9 +1,9 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const main3D = {
id: 'main3D',
locked: true,
name: '3D main',
name: i18n.t('Hps:3D main'),
icon: 'layout-advanced-3d-main',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
+2 -2
View File
@@ -1,5 +1,5 @@
import { Types } from '@ohif/core';
import i18n from 'i18next';
export const VOI_SYNC_GROUP = {
type: 'voi',
id: 'mpr',
@@ -22,7 +22,7 @@ export const HYDRATE_SEG_SYNC_GROUP = {
export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'MPR',
name: i18n.t('Hps:MPR'),
locked: true,
icon: 'layout-advanced-mpr',
isPreset: true,
@@ -1,9 +1,9 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const mprAnd3DVolumeViewport = {
id: 'mprAnd3DVolumeViewport',
locked: true,
name: 'mpr',
name: i18n.t('Hps:mpr'),
createdDate: '2023-03-15T10:29:44.894Z',
modifiedDate: '2023-03-15T10:29:44.894Z',
availableTo: {},
+2 -1
View File
@@ -1,9 +1,10 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const only3D = {
id: 'only3D',
locked: true,
name: '3D only',
name: i18n.t('Hps:3D only'),
icon: 'layout-advanced-3d-only',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
+2 -2
View File
@@ -1,9 +1,9 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const primary3D = {
id: 'primary3D',
locked: true,
name: '3D primary',
name: i18n.t('Hps:3D primary'),
icon: 'layout-advanced-3d-primary',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
@@ -1,9 +1,9 @@
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
import i18n from 'i18next';
export const primaryAxial = {
id: 'primaryAxial',
locked: true,
name: 'Axial Primary',
name: i18n.t('Hps:Axial Primary'),
icon: 'layout-advanced-axial-primary',
isPreset: true,
createdDate: '2023-03-15T10:29:44.894Z',
+6
View File
@@ -44,6 +44,7 @@ import {
usePositionPresentationStore,
useSegmentationPresentationStore,
useSynchronizersStore,
useSelectedSegmentationsForViewportStore,
} from './stores';
import { useToggleOneUpViewportGridStore } from '@ohif/extension-default';
import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations';
@@ -109,6 +110,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
segmentationService.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
]);
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
@@ -153,6 +155,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
useSynchronizersStore.getState().clearSynchronizersStore();
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
useSelectedSegmentationsForViewportStore
.getState()
.clearSelectedSegmentationsForViewportStore();
segmentationService.removeAllSegmentations();
},
@@ -256,6 +261,7 @@ export {
usePositionPresentationStore,
useSegmentationPresentationStore,
useSynchronizersStore,
useSelectedSegmentationsForViewportStore,
Enums,
useMeasurements,
useActiveViewportSegmentationRepresentations,
+6
View File
@@ -106,6 +106,12 @@ export default async function init({
colorbarService.EVENTS.STATE_CHANGED,
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
segmentationService.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
]);
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
@@ -41,6 +41,10 @@ import {
SegmentSelectTool,
RegionSegmentPlusTool,
SegmentLabelTool,
LivewireContourSegmentationTool,
SculptorTool,
SplineContourSegmentationTool,
LabelMapEditWithContourTool,
} from '@cornerstonejs/tools';
import { LabelmapSlicePropagationTool, MarkerLabelmapTool } from '@cornerstonejs/ai';
import * as polySeg from '@cornerstonejs/polymorphic-segmentation';
@@ -109,6 +113,10 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(LabelmapSlicePropagationTool);
addTool(MarkerLabelmapTool);
addTool(RegionSegmentPlusTool);
addTool(LivewireContourSegmentationTool);
addTool(SculptorTool);
addTool(SplineContourSegmentationTool);
addTool(LabelMapEditWithContourTool);
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
textBoxFontSize: '15px',
@@ -168,6 +176,10 @@ const toolNames = {
LabelmapSlicePropagation: LabelmapSlicePropagationTool.toolName,
MarkerLabelmap: MarkerLabelmapTool.toolName,
RegionSegmentPlus: RegionSegmentPlusTool.toolName,
LivewireContourSegmentation: LivewireContourSegmentationTool.toolName,
SculptorTool: SculptorTool.toolName,
SplineContourSegmentation: SplineContourSegmentationTool.toolName,
LabelMapEditWithContourTool: LabelMapEditWithContourTool.toolName,
};
export { toolNames };
@@ -450,7 +450,7 @@ const connectMeasurementServiceToTools = ({
if (measurement?.metadata?.referencedImageId) {
imageId = measurement.metadata.referencedImageId;
frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber;
} else {
} else if (instance) {
imageId = dataSource.getImageIdsForInstance({ instance });
}
@@ -78,6 +78,7 @@ export default function PanelMeasurement(props): React.ReactNode {
);
return cloned;
}
// Need to merge defaults on the content props to ensure they get passed to children
return <StudyMeasurements items={displayMeasurements} />;
}
@@ -1,16 +1,92 @@
import React from 'react';
import { SegmentationTable } from '@ohif/ui-next';
import React, { useCallback, useEffect } from 'react';
import {
IconPresentationProvider,
Popover,
PopoverAnchor,
PopoverContent,
SegmentationTable,
ToolSettings,
} from '@ohif/ui-next';
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
import { metaData, cache } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core/src';
import { useActiveToolOptions, useSystem } from '@ohif/core/src';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { Toolbar, useUIStateStore } from '@ohif/extension-default';
import SegmentationUtilityButton from '../components/SegmentationUtilityButton';
import { useSelectedSegmentationsForViewportStore } from '../stores';
import {
hasExportableLabelMapData,
hasExportableContourData,
} from '../utils/segmentationExportUtils';
export default function PanelSegmentation({ children }: withAppTypes) {
type PanelSegmentationProps = {
children?: React.ReactNode;
// The representation type for this segmentation panel. Undefined means all types.
segmentationRepresentationType?: SegmentationRepresentations;
} & withAppTypes;
export default function PanelSegmentation({
children,
segmentationRepresentationType,
}: PanelSegmentationProps) {
const { commandsManager, servicesManager } = useSystem();
const { customizationService, displaySetService } = servicesManager.services;
const {
customizationService,
displaySetService,
viewportGridService,
toolbarService,
segmentationService,
} = servicesManager.services;
const { activeViewportId } = viewportGridService.getState();
const utilitiesSectionMap = {
[SegmentationRepresentations.Labelmap]: toolbarService.sections.labelMapSegmentationUtilities,
[SegmentationRepresentations.Contour]: toolbarService.sections.contourSegmentationUtilities,
};
const selectedSegmentationsForViewportMap = useSelectedSegmentationsForViewportStore(
store => store.selectedSegmentationsForViewport[activeViewportId]
);
const selectedSegmentationIdForType = segmentationRepresentationType
? selectedSegmentationsForViewportMap?.get(segmentationRepresentationType)
: segmentationService?.getActiveSegmentation(activeViewportId)?.segmentationId;
const buttonSection = utilitiesSectionMap[segmentationRepresentationType];
const { activeToolOptions: activeUtilityOptions } = useActiveToolOptions({
buttonSectionId: buttonSection,
});
const { segmentationsWithRepresentations, disabled } =
useActiveViewportSegmentationRepresentations();
const setUIState = useUIStateStore(store => store.setUIState);
// useEffect for handling clicks on any of the non-active viewports.
// The ViewportGrid stops the propagation of pointer/mouse events
// for non-active viewports so the Popover below
// is not closed when clicking on any of the non-active viewports.
useEffect(() => {
setUIState('activeSegmentationUtility', null);
toolbarService.refreshToolbarState({ viewportId: activeViewportId });
}, [activeViewportId, setUIState, toolbarService]);
// The callback for handling clicks outside of the Popover and, the SegmentationUtilityButton
// that triggered it to open. Clicks outside those components must close the Popover.
// The Popover is made visible whenever the options associated with the
// activeSegmentationUtility exist. Thus clearing the activeSegmentationUtility
// clears the associated options and will keep the Popover closed.
const handlePopoverOpenChange = useCallback(
(open: boolean) => {
if (!open) {
setUIState('activeSegmentationUtility', null);
toolbarService.refreshToolbarState({ viewportId: activeViewportId });
}
},
[activeViewportId, setUIState, toolbarService]
);
// Extract customization options
const segmentationTableMode = customizationService.getCustomization(
'panelSegmentation.tableMode'
@@ -35,6 +111,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
@@ -51,6 +128,14 @@ export default function PanelSegmentation({ children }: withAppTypes) {
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onSegmentCopy:
segmentationRepresentationType === SegmentationRepresentations.Contour
? (segmentationId, segmentIndex) => {
commandsManager.run('copyContourSegment', {
sourceSegmentInfo: { segmentationId, segmentIndex },
});
}
: undefined,
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
@@ -96,52 +181,26 @@ export default function PanelSegmentation({ children }: withAppTypes) {
};
// Generate export options
// Map each segmentation to an export option for it.
// A segmentation is exportable if it has any labelmap or contour data.
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
const { representationData, segmentationId } = segmentation;
const { Labelmap } = representationData;
const { Labelmap, Contour } = representationData;
if (!Labelmap) {
if (!Labelmap && !Contour) {
return { segmentationId, isExportable: true };
}
// Check if any segments have anything drawn in any of the viewports
const hasAnySegmentData = (() => {
const imageIds = Labelmap.imageIds;
if (!imageIds?.length) return false;
for (const imageId of imageIds) {
const pixelData = cache.getImage(imageId)?.getPixelData();
if (!pixelData) continue;
for (let i = 0; i < pixelData.length; i++) {
if (pixelData[i] !== 0) return true;
}
}
return false;
})();
if (!hasAnySegmentData) {
if (
!hasExportableLabelMapData(Labelmap, displaySetService) &&
!hasExportableContourData(Contour)
) {
return { segmentationId, isExportable: false };
}
const referencedImageIds = Labelmap.referencedImageIds;
const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId);
if (!instance) {
return { segmentationId, isExportable: false };
}
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
const SeriesInstanceUID = instance.SeriesInstanceUID;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
return {
segmentationId,
isExportable: displaySet?.isReconstructable,
isExportable: true,
};
});
@@ -150,15 +209,34 @@ export default function PanelSegmentation({ children }: withAppTypes) {
disabled,
data: segmentationsWithRepresentations,
mode: segmentationTableMode,
title: 'Segmentations',
title: `${segmentationRepresentationType ? `${segmentationRepresentationType} ` : ''}Segmentations`,
exportOptions,
disableEditing,
onSegmentationAdd,
showAddSegment,
renderInactiveSegmentations: handlers.getRenderInactiveSegmentations(),
segmentationRepresentationType,
selectedSegmentationIdForType,
...handlers,
};
const renderUtilitiesToolbar = () => {
if (!buttonSection) {
return null;
}
return (
<IconPresentationProvider
size="large"
IconContainer={SegmentationUtilityButton}
>
<div className="flex flex-wrap gap-[3px] bg-transparent pb-[2px] pl-[8px] pt-[6px]">
<Toolbar buttonSection={buttonSection} />
</div>
</IconPresentationProvider>
);
};
const renderSegments = () => {
return (
<SegmentationTable.Segments>
@@ -175,6 +253,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
if (tableProps.mode === 'collapsed') {
return (
<SegmentationTable.Collapsed>
{renderUtilitiesToolbar()}
<SegmentationTable.Collapsed.Header>
<SegmentationTable.Collapsed.DropdownMenu>
<CustomDropdownMenuContent />
@@ -193,6 +272,7 @@ export default function PanelSegmentation({ children }: withAppTypes) {
return (
<>
<SegmentationTable.Expanded>
{renderUtilitiesToolbar()}
<SegmentationTable.Expanded.Header>
<SegmentationTable.Expanded.DropdownMenu>
<CustomDropdownMenuContent />
@@ -211,11 +291,27 @@ export default function PanelSegmentation({ children }: withAppTypes) {
};
return (
<SegmentationTable {...tableProps}>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{renderModeContent()}
</SegmentationTable>
<Popover
open={!!activeUtilityOptions}
onOpenChange={handlePopoverOpenChange}
>
<PopoverAnchor>
<SegmentationTable {...tableProps}>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{renderModeContent()}
</SegmentationTable>
</PopoverAnchor>
{activeUtilityOptions && (
<PopoverContent
side="left"
align="start"
className="w-auto"
>
<ToolSettings options={activeUtilityOptions} />
</PopoverContent>
)}
</Popover>
);
}
@@ -9,30 +9,34 @@ import {
utilities as csUtils,
metaData,
} from '@cornerstonejs/core';
import { ViewportType } from '@cornerstonejs/core/enums';
import {
Enums as csToolsEnums,
segmentation as cstSegmentation,
Types as cstTypes,
} from '@cornerstonejs/tools';
import { PubSubService, Types as OHIFTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { easeInOutBell, easeInOutBellRelative } from '../../utils/transitions';
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT';
import { getNextColorLUTIndex } from '@cornerstonejs/tools/segmentation/getNextColorLUTIndex';
import { Segment } from '@cornerstonejs/tools/types/SegmentationStateTypes';
import { ContourStyle, LabelmapStyle, SurfaceStyle } from '@cornerstonejs/tools/types';
import { ViewportType } from '@cornerstonejs/core/enums';
import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation';
import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/segmentation/updateLabelmapSegmentationImageReferences';
import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents';
import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap';
import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
import { VOLUME_LOADER_SCHEME } from '../../constants';
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation';
import { EasingFunctionEnum, EasingFunctionMap } from '../../utils/transitions';
const {
Labelmap: LABELMAP,
Contour: CONTOUR,
Surface: SURFACE,
} = csToolsEnums.SegmentationRepresentations;
const {
getLabelmapImageIds,
helpers: { convertStackToVolumeLabelmap },
state: { addColorLUT, updateLabelmapSegmentationImageReferences },
triggerSegmentationEvents: { triggerSegmentationRepresentationModified },
} = cstSegmentation;
export type SegmentRepresentation = {
segmentIndex: number;
@@ -77,6 +81,8 @@ const EVENTS = {
SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete',
// loading completed for all segments
SEGMENTATION_LOADING_COMPLETE: 'event::segmentation_loading_complete',
// fired when a contour annotation cut merge process is completed
ANNOTATION_CUT_MERGE_PROCESS_COMPLETED: 'event::annotation_cut_merge_process_completed',
};
const VALUE_TYPES = {};
@@ -149,13 +155,11 @@ class SegmentationService extends PubSubService {
const representations = this.getSegmentationRepresentations(viewportId);
for (const representation of representations) {
const { segmentationId } = representation;
if (!representation) {
continue;
}
const { type } = representation;
const { segmentationId, type } = representation;
segmentationsMap.set(segmentationId, {
segmentationId,
@@ -212,7 +216,7 @@ class SegmentationService extends PubSubService {
viewportId: string,
specifier: {
segmentationId?: string;
type?: SegmentationRepresentations;
type?: csToolsEnums.SegmentationRepresentations;
} = {}
): SegmentationRepresentation[] {
// Get all representations for the viewportId
@@ -245,9 +249,19 @@ class SegmentationService extends PubSubService {
this._onSegmentationDataModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED,
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED,
this._onSegmentationModifiedFromSource
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED,
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.removeEventListener(
@@ -283,11 +297,13 @@ class SegmentationService extends PubSubService {
const colorLUTIndex = this._segmentationIdToColorLUTIndexMap.get(segmentationId);
const defaultRepresentationType = csToolsEnums.SegmentationRepresentations.Labelmap;
let representationTypeToUse = type || defaultRepresentationType;
let isConverted = false;
if (type === csToolsEnums.SegmentationRepresentations.Labelmap) {
const defaultRepresentationType: csToolsEnums.SegmentationRepresentations =
csViewport.type === ViewportType.VOLUME_3D ? SURFACE : LABELMAP;
let representationTypeToUse = type || defaultRepresentationType;
if (representationTypeToUse === LABELMAP) {
const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
csViewport,
segmentation
@@ -336,6 +352,39 @@ class SegmentationService extends PubSubService {
FrameOfReferenceUID?: string;
label?: string;
}
): Promise<string> {
return this._createSegmentationForDisplaySet(displaySet, LABELMAP, options);
}
public async createContourForDisplaySet(
displaySet: AppTypes.DisplaySet,
options?: {
segmentationId?: string;
segments?: { [segmentIndex: number]: Partial<cstTypes.Segment> };
FrameOfReferenceUID?: string;
label?: string;
}
): Promise<string> {
return this._createSegmentationForDisplaySet(displaySet, CONTOUR, options);
}
/**
* Private method to create segmentation for a display set with the specified type
*
* @param displaySet - The display set to create the segmentation for
* @param segmentationType - The type of segmentation (SegmentationRepresentations enum)
* @param options - Optional parameters for creating the segmentation
* @returns A promise that resolves to the created segmentation ID
*/
private async _createSegmentationForDisplaySet(
displaySet: AppTypes.DisplaySet,
segmentationType: SegmentationRepresentations,
options?: {
segmentationId?: string;
segments?: { [segmentIndex: number]: Partial<cstTypes.Segment> };
FrameOfReferenceUID?: string;
label?: string;
}
): Promise<string> {
// Todo: random does not makes sense, make this better, like
// labelmap 1, 2, 3 etc
@@ -354,14 +403,14 @@ class SegmentationService extends PubSubService {
const derivedImages = await imageLoader.createAndCacheDerivedLabelmapImages(referenceImageIds);
const segs = this.getSegmentations();
const label = options.label || `Segmentation ${segs.length + 1}`;
const label = options?.label || `Segmentation ${segs.length + 1}`;
const segImageIds = derivedImages.map(image => image.imageId);
const segmentationPublicInput: cstTypes.SegmentationPublicInput = {
segmentationId,
representation: {
type: LABELMAP,
type: segmentationType,
data: {
imageIds: segImageIds,
// referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet),
@@ -371,7 +420,7 @@ class SegmentationService extends PubSubService {
config: {
label,
segments:
options.segments && Object.keys(options.segments).length > 0
options?.segments && Object.keys(options.segments).length > 0
? options.segments
: {
1: {
@@ -393,7 +442,7 @@ class SegmentationService extends PubSubService {
segDisplaySet,
options: {
segmentationId?: string;
type: SegmentationRepresentations;
type: csToolsEnums.SegmentationRepresentations;
} = {
type: LABELMAP,
}
@@ -503,9 +552,7 @@ class SegmentationService extends PubSubService {
};
});
// get next color lut index
const colorLUTIndex = getNextColorLUTIndex();
addColorLUT(colorLUT, colorLUTIndex);
const colorLUTIndex = addColorLUT(colorLUT);
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
@@ -540,7 +587,7 @@ class SegmentationService extends PubSubService {
rtDisplaySet,
options: {
segmentationId?: string;
type: SegmentationRepresentations;
type: csToolsEnums.SegmentationRepresentations;
} = {
type: CONTOUR,
}
@@ -616,7 +663,9 @@ class SegmentationService extends PubSubService {
const colorLUT = [[0, 0, 0, 0]]; // First entry is transparent for index 0
// Process each segment similarly to the SEG function
for (const rtStructData of allRTStructData) {
for (let i = 0; i < allRTStructData.length; i++) {
const rtStructData = allRTStructData[i];
const { data, id, color, segmentIndex, geometryId, group } = rtStructData;
// Add the color to the colorLUT array
@@ -665,8 +714,7 @@ class SegmentationService extends PubSubService {
}
// Create and register the colorLUT
const colorLUTIndex = getNextColorLUTIndex();
addColorLUT(colorLUT, colorLUTIndex);
const colorLUTIndex = addColorLUT(colorLUT);
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
// Assign processed segments to segmentation config
@@ -763,7 +811,7 @@ class SegmentationService extends PubSubService {
public hasCustomStyles(specifier: {
viewportId: string;
segmentationId: string;
type: SegmentationRepresentations;
type: csToolsEnums.SegmentationRepresentations;
}): boolean {
return cstSegmentation.config.style.hasCustomStyle(specifier);
}
@@ -771,7 +819,7 @@ class SegmentationService extends PubSubService {
public getStyle = (specifier: {
viewportId: string;
segmentationId: string;
type: SegmentationRepresentations;
type: csToolsEnums.SegmentationRepresentations;
segmentIndex?: number;
}) => {
const style = cstSegmentation.config.style.getStyle(specifier);
@@ -781,12 +829,12 @@ class SegmentationService extends PubSubService {
public setStyle = (
specifier: {
type: SegmentationRepresentations;
type: csToolsEnums.SegmentationRepresentations;
viewportId?: string;
segmentationId?: string;
segmentIndex?: number;
},
style: LabelmapStyle | ContourStyle | SurfaceStyle
style: cstTypes.LabelmapStyle | cstTypes.ContourStyle | cstTypes.SurfaceStyle
) => {
cstSegmentation.config.style.setStyle(specifier, style);
};
@@ -795,6 +843,15 @@ class SegmentationService extends PubSubService {
cstSegmentation.config.style.resetToGlobalStyle();
};
public getNextAvailableSegmentIndex(segmentationId: string): number {
const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
// grab the next available segment index based on the object keys,
// so basically get the highest segment index value + 1
const segmentKeys = Object.keys(csSegmentation.segments);
return segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1;
}
/**
* Adds a new segment to the specified segmentation.
* @param segmentationId - The ID of the segmentation to add the segment to.
@@ -828,10 +885,7 @@ class SegmentationService extends PubSubService {
let segmentIndex = config.segmentIndex;
if (!segmentIndex) {
// grab the next available segment index based on the object keys,
// so basically get the highest segment index value + 1
const segmentKeys = Object.keys(csSegmentation.segments);
segmentIndex = segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1;
segmentIndex = this.getNextAvailableSegmentIndex(segmentationId);
}
// update the segmentation
@@ -904,7 +958,7 @@ class SegmentationService extends PubSubService {
segmentationId: string,
segmentIndex: number,
isVisible: boolean,
type?: SegmentationRepresentations
type?: csToolsEnums.SegmentationRepresentations
): void {
this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type);
}
@@ -941,7 +995,7 @@ class SegmentationService extends PubSubService {
viewportId: string,
segmentationId: string,
segmentIndex: number,
type: SegmentationRepresentations
type: csToolsEnums.SegmentationRepresentations
): void {
const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility(
viewportId,
@@ -972,6 +1026,12 @@ class SegmentationService extends PubSubService {
segmentIndex: number,
color: csTypes.Color
): void {
const segmentationRepresentations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
});
const { colorLUTIndex } = segmentationRepresentations[0];
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
cstSegmentation.config.color.setSegmentIndexColor(
viewportId,
segmentationId,
@@ -1013,7 +1073,7 @@ class SegmentationService extends PubSubService {
public getLabelmapVolume(segmentationId: string) {
const csSegmentation = cstSegmentation.state.getSegmentation(segmentationId);
const labelmapData = csSegmentation.representationData[
SegmentationRepresentations.Labelmap
LABELMAP
] as cstTypes.LabelmapToolOperationDataVolume;
if (!labelmapData || !labelmapData.volumeId) {
@@ -1115,7 +1175,10 @@ class SegmentationService extends PubSubService {
*/
public toggleSegmentationRepresentationVisibility = (
viewportId: string,
{ segmentationId, type }: { segmentationId: string; type: SegmentationRepresentations }
{
segmentationId,
type,
}: { segmentationId: string; type: csToolsEnums.SegmentationRepresentations }
): void => {
this._toggleSegmentationRepresentationVisibility(viewportId, segmentationId, type);
};
@@ -1164,7 +1227,7 @@ class SegmentationService extends PubSubService {
viewportId: string,
specifier: {
segmentationId?: string;
type?: SegmentationRepresentations;
type?: csToolsEnums.SegmentationRepresentations;
} = {}
): void {
cstSegmentation.removeSegmentationRepresentations(viewportId, specifier);
@@ -1178,7 +1241,7 @@ class SegmentationService extends PubSubService {
highlightSegment = true,
animationLength = 750,
highlightHideOthers = false,
highlightFunctionType = 'ease-in-out' // todo: make animation functions configurable from outside
animationFunctionType: EasingFunctionEnum = EasingFunctionEnum.EASE_IN_OUT
): void {
const center = this._getSegmentCenter(segmentationId, segmentIndex);
if (!center) {
@@ -1204,7 +1267,8 @@ class SegmentationService extends PubSubService {
viewportId,
highlightAlpha,
animationLength,
highlightHideOthers
highlightHideOthers,
animationFunctionType
);
});
}
@@ -1216,7 +1280,7 @@ class SegmentationService extends PubSubService {
alpha = 0.9,
animationLength = 750,
hideOthers = true,
highlightFunctionType = 'ease-in-out'
animationFunctionType: EasingFunctionEnum = EasingFunctionEnum.EASE_IN_OUT
): void {
if (this.highlightIntervalId) {
clearInterval(this.highlightIntervalId);
@@ -1249,7 +1313,8 @@ class SegmentationService extends PubSubService {
segments,
viewportId,
animationLength,
representation
representation,
animationFunctionType
);
});
}
@@ -1274,7 +1339,7 @@ class SegmentationService extends PubSubService {
private _setSegmentationRepresentationVisibility(
viewportId: string,
segmentationId: string,
type: SegmentationRepresentations,
type: csToolsEnums.SegmentationRepresentations,
isVisible: boolean
): void {
const representations = this.getSegmentationRepresentations(viewportId, {
@@ -1334,14 +1399,17 @@ class SegmentationService extends PubSubService {
private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) {
if (csViewport.type === ViewportType.VOLUME_3D) {
return { representationTypeToUse: SegmentationRepresentations.Surface, isConverted: false };
return {
representationTypeToUse: SURFACE,
isConverted: false,
};
} else {
await this.handleVolumeViewport(
csViewport as csTypes.IVolumeViewport,
segmentation,
isVolumeSegmentation
);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
return { representationTypeToUse: LABELMAP, isConverted: false };
}
}
@@ -1351,14 +1419,17 @@ class SegmentationService extends PubSubService {
isVolumeSegmentation: boolean,
viewportId: string,
segmentationId: string
): Promise<{ representationTypeToUse: SegmentationRepresentations; isConverted: boolean }> {
): Promise<{
representationTypeToUse: csToolsEnums.SegmentationRepresentations;
isConverted: boolean;
}> {
if (isVolumeSegmentation) {
const isConverted = await this.convertStackToVolumeViewport(csViewport);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
return { representationTypeToUse: LABELMAP, isConverted };
}
if (updateLabelmapSegmentationImageReferences(viewportId, segmentationId)) {
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
return { representationTypeToUse: LABELMAP, isConverted: false };
}
const isConverted = await this.attemptStackToVolumeConversion(
@@ -1368,7 +1439,7 @@ class SegmentationService extends PubSubService {
segmentationId
);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
return { representationTypeToUse: LABELMAP, isConverted };
}
private async _addSegmentationRepresentation(
@@ -1479,11 +1550,7 @@ class SegmentationService extends PubSubService {
segImage.FrameOfReferenceUID === frameOfReferenceUID
) {
const isConverted = await this.convertStackToVolumeViewport(viewport);
triggerSegmentationRepresentationModified(
viewportId,
segmentationId,
SegmentationRepresentations.Labelmap
);
triggerSegmentationRepresentationModified(viewportId, segmentationId, LABELMAP);
return isConverted;
}
@@ -1601,6 +1668,11 @@ class SegmentationService extends PubSubService {
csToolsEnums.Events.SEGMENTATION_ADDED,
this._onSegmentationAddedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED,
this._onAnnotationCutMergeProcessCompletedFromSource
);
}
private getCornerstoneSegmentation(segmentationId: string) {
@@ -1611,10 +1683,11 @@ class SegmentationService extends PubSubService {
segmentIndex: number,
alpha: number,
hideOthers: boolean,
segments: Segment[],
segments: cstTypes.Segment[],
viewportId: string,
animationLength: number,
representation: cstTypes.SegmentationRepresentation
representation: cstTypes.SegmentationRepresentation,
animationFunctionType: EasingFunctionEnum
) {
const { segmentationId } = representation;
const newSegmentSpecificConfig = {
@@ -1648,6 +1721,8 @@ class SegmentationService extends PubSubService {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / animationLength, 1);
const easingFunction = EasingFunctionMap.get(animationFunctionType);
cstSegmentation.config.style.setStyle(
{
segmentationId,
@@ -1655,7 +1730,7 @@ class SegmentationService extends PubSubService {
type: LABELMAP,
},
{
fillAlpha: easeInOutBell(progress, fillAlpha),
fillAlpha: easingFunction(progress, fillAlpha),
}
);
@@ -1680,21 +1755,20 @@ class SegmentationService extends PubSubService {
segmentIndex: number,
alpha: number,
hideOthers: boolean,
segments: Segment[],
segments: cstTypes.Segment[],
viewportId: string,
animationLength: number,
representation: cstTypes.SegmentationRepresentation
representation: cstTypes.SegmentationRepresentation,
animationFunctionType: EasingFunctionEnum
) {
const { segmentationId } = representation;
const startTime = performance.now();
const prevStyle = cstSegmentation.config.style.getStyle({
type: CONTOUR,
}) as ContourStyle;
}) as cstTypes.ContourStyle;
const prevOutlineWidth = prevStyle.outlineWidth;
// make this configurable
const baseline = Math.max(prevOutlineWidth * 3.5, 5);
const animate = (currentTime: number) => {
const progress = (currentTime - startTime) / animationLength;
@@ -1703,7 +1777,8 @@ class SegmentationService extends PubSubService {
return;
}
const reversedProgress = easeInOutBellRelative(progress, baseline, prevOutlineWidth);
const OUTLINE_ANIMATION_SCALE_FACTOR = 5;
const easingFunction = EasingFunctionMap.get(animationFunctionType);
cstSegmentation.config.style.setStyle(
{
@@ -1712,7 +1787,7 @@ class SegmentationService extends PubSubService {
type: CONTOUR,
},
{
outlineWidth: reversedProgress,
outlineWidth: easingFunction(progress, prevOutlineWidth, OUTLINE_ANIMATION_SCALE_FACTOR),
}
);
@@ -1725,24 +1800,18 @@ class SegmentationService extends PubSubService {
private _toggleSegmentationRepresentationVisibility = (
viewportId: string,
segmentationId: string,
type: SegmentationRepresentations
type: csToolsEnums.SegmentationRepresentations
): void => {
const representations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
type,
});
const representation = representations[0];
const segmentsHidden = cstSegmentation.config.visibility.getHiddenSegmentIndices(viewportId, {
segmentationId,
type: representation.type,
type,
});
const currentVisibility = segmentsHidden.size === 0;
this._setSegmentationRepresentationVisibility(
viewportId,
segmentationId,
representation.type,
type,
!currentVisibility
);
};
@@ -1757,7 +1826,10 @@ class SegmentationService extends PubSubService {
return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
}
private _getSegmentCenter(segmentationId, segmentIndex) {
private _getSegmentCenter(
segmentationId: string,
segmentIndex: number
): { image?: csTypes.Point3; world: csTypes.Point3 } | undefined {
const segmentation = this.getSegmentation(segmentationId);
if (!segmentation) {
@@ -1768,19 +1840,17 @@ class SegmentationService extends PubSubService {
const { cachedStats } = segments[segmentIndex];
if (!cachedStats || !cachedStats?.center || !cachedStats?.namedStats?.center) {
return;
if (cachedStats?.center) {
const { center } = cachedStats;
return center as { image: csTypes.Point3; world: csTypes.Point3 };
}
const { center } = cachedStats;
if (!center) {
if (cachedStats?.namedStats?.center) {
return {
world: cachedStats.namedStats.center.value,
};
}
return center;
}
private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) {
@@ -1792,7 +1862,7 @@ class SegmentationService extends PubSubService {
segmentationId: string,
segmentIndex: number,
isVisible: boolean,
type?: SegmentationRepresentations
type?: csToolsEnums.SegmentationRepresentations
) {
cstSegmentation.config.visibility.setSegmentIndexVisibility(
viewportId,
@@ -1855,6 +1925,13 @@ class SegmentationService extends PubSubService {
segmentationId,
});
};
private _onAnnotationCutMergeProcessCompletedFromSource = evt => {
const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, {
segmentationId,
});
};
}
export default SegmentationService;
@@ -65,13 +65,11 @@ const segmentationRepresentationModifiedCallback = async (
return;
}
// whatever type the source viewport has, we need to add that to the target viewport
const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations(
sourceViewport.viewportId,
{ segmentationId }
);
const type = sourceViewportRepresentation[0].type;
// Ensure the segmentation representation aligns with the target viewport type.
const type: Enums.SegmentationRepresentations =
viewport.type === CoreEnums.ViewportType.VOLUME_3D
? Enums.SegmentationRepresentations.Surface
: Enums.SegmentationRepresentations.Labelmap;
await segmentationService.addSegmentationRepresentation(targetViewportId, {
segmentationId,
@@ -714,7 +714,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// is being used to navigate to the initial view position for measurement
// navigation and other navigation forcing specific views.
let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? <number>initialImageIndex;
presentations?.positionPresentation?.initialImageIndex ?? (initialImageIndex as number);
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
@@ -933,6 +933,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
const displaySetModality = displaySet?.Modality;
// seems like a hack but we need the actor to be ready first before
// we set the properties
const timeoutViewportCallback = (callback: () => void) => setTimeout(callback, 0);
// filter overlay display sets (e.g. segmentation) since they will get handled below via the segmentation service
const filteredVolumeInputArray = volumeInputArray
.map((volumeInput, index) => {
@@ -984,12 +988,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const backgroundDisplaySet = displaySetService.getDisplaySetsBy(
displaySet =>
!displaySet.isOverlayDisplaySet &&
displaySet.images.some(image => image.imageId === sampleImageId)
displaySet.images?.some(image => image.imageId === sampleImageId)
);
if (backgroundDisplaySet.length !== 1) {
throw new Error('Background display set not found');
}
if (viewport.type === csEnums.ViewportType.VOLUME_3D) {
timeoutViewportCallback(() => {
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.id,
displaySetInstanceUIDs: [backgroundDisplaySet[0].displaySetInstanceUID],
});
});
}
}
});
}
@@ -1006,15 +1019,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewport.render();
volumesProperties.forEach(({ properties, volumeId }) => {
setTimeout(() => {
// seems like a hack but we need the actor to be ready first before
// we set the properties
timeoutViewportCallback(() => {
viewport.setProperties(properties, volumeId);
viewport.render();
}, 0);
});
});
this.setPresentations(viewport.id, presentations, viewportInfo);
this.setPresentations(viewport.id, presentations);
if (!presentations.positionPresentation) {
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
@@ -1335,10 +1346,18 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
segmentationPresentation.forEach((presentationItem: SegmentationPresentationItem) => {
const { segmentationId, type, hydrated } = presentationItem;
const { Labelmap, Surface } = csToolsEnums.SegmentationRepresentations;
const isVolume3D = viewport.type === csEnums.ViewportType.VOLUME_3D;
// Determine the appropriate segmentation representation for the viewport.
// If the current type is Surface but the viewport is not 3D, fallback to Labelmap.
// Otherwise, use the existing type.
const representationType = type === Surface && !isVolume3D ? Labelmap : type;
if (hydrated) {
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
type,
type: representationType,
});
}
});
@@ -2,3 +2,4 @@ export { useLutPresentationStore } from './useLutPresentationStore';
export { usePositionPresentationStore } from './usePositionPresentationStore';
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
export { useSynchronizersStore } from './useSynchronizersStore';
export { useSelectedSegmentationsForViewportStore } from './useSelectedSegmentationsForViewportStore';
@@ -0,0 +1,85 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { SelectedSegmentationByTypeMap } from '../types/SelectedSegmentationByTypeMap';
const PRESENTATION_TYPE_ID = 'selectedSegmentationsForViewportId';
const DEBUG_STORE = false;
/**
* Represents the state and actions for managing selected segmentations for a viewport by representation type.
*/
type SelectedSegmentationsForViewportState = {
/**
* Stores a mapping from viewport id to a map of representation type to segmentation id.
*/
selectedSegmentationsForViewport: Record<string, SelectedSegmentationByTypeMap>;
/**
* Sets the selected segmentations for a given viewport id.
*
* @param key - The viewport id.
* @param value - The `SelectedSegmentationByTypeMap` to associate with the viewport id.
*/
setSelectedSegmentationsForViewport: (key: string, value: SelectedSegmentationByTypeMap) => void;
/**
* Clears all selected segmentations for all viewports.
*/
clearSelectedSegmentationsForViewportStore: () => void;
/**
* Type identifier for the store.
*/
type: string;
};
/**
* Creates the Selected Segmentations For Viewport store.
*
* @param set - The zustand set function.
* @returns The selected segmentations for viewport store state and actions.
*/
const createSelectedSegmentationsForViewportStore = (
set
): SelectedSegmentationsForViewportState => ({
selectedSegmentationsForViewport: {},
type: PRESENTATION_TYPE_ID,
/**
* Sets the selected segmentations for a given viewport id.
*/
setSelectedSegmentationsForViewport: (key, value) =>
set(
state => ({
selectedSegmentationsForViewport: {
...state.selectedSegmentationsForViewport,
[key]: value,
},
}),
false,
'setSelectedSegmentationsForViewport'
),
/**
* Clears all selected segmentations for all viewports.
*/
clearSelectedSegmentationsForViewportStore: () =>
set(
{ selectedSegmentationsForViewport: {} },
false,
'clearSelectedSegmentationsForViewportStore'
),
});
/**
* Zustand store for managing selected segmentations for a viewport by representation type.
* Applies devtools middleware when DEBUG_STORE is enabled.
*/
export const useSelectedSegmentationsForViewportStore =
create<SelectedSegmentationsForViewportState>()(
DEBUG_STORE
? devtools(createSelectedSegmentationsForViewportStore, {
name: 'SelectedSegmentationsForViewportStore',
})
: createSelectedSegmentationsForViewportStore
);
@@ -0,0 +1,3 @@
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
export type SelectedSegmentationByTypeMap = Map<SegmentationRepresentations, string>;
@@ -0,0 +1,74 @@
import { utilities as csUtils } from '@cornerstonejs/core';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import i18n from '@ohif/i18n';
import { ServicesManager } from '@ohif/core';
function _createDefaultSegments(createInitialSegment?: boolean) {
return createInitialSegment
? {
1: {
label: `${i18n.t('Tools:Segment')} 1`,
active: true,
},
}
: {};
}
type CreateSegmentationForViewportOptions = {
displaySetInstanceUID?: string;
label?: string;
segmentationId?: string;
createInitialSegment?: boolean;
};
type CreateSegmentationForViewportParams = {
viewportId: string;
options?: CreateSegmentationForViewportOptions;
segmentationType: SegmentationRepresentations;
};
/**
* Creates a segmentation for the active viewport
*
* The created segmentation will be registered as a display set and also added
* as a segmentation representation to the viewport.
*/
export async function createSegmentationForViewport(
servicesManager: ServicesManager,
{ viewportId, options = {}, segmentationType }: CreateSegmentationForViewportParams
): Promise<string> {
const { viewportGridService, displaySetService, segmentationService } = servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
const segs = segmentationService.getSegmentations();
const label = options.label || `${i18n.t('Tools:Segmentation')} ${segs.length + 1}`;
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const segmentationCreationOptions = {
label,
segmentationId,
segments: _createDefaultSegments(options.createInitialSegment),
};
// This will create the segmentation and register it as a display set
const generatedSegmentationId = await (segmentationType === SegmentationRepresentations.Labelmap
? segmentationService.createLabelmapForDisplaySet(displaySet, segmentationCreationOptions)
: segmentationService.createContourForDisplaySet(displaySet, segmentationCreationOptions));
// Also add the segmentation representation to the viewport
await segmentationService.addSegmentationRepresentation(viewportId, {
segmentationId,
type: segmentationType,
});
return generatedSegmentationId;
}
@@ -0,0 +1,125 @@
import { findNearbyToolData } from './findNearbyToolData';
describe('findNearbyToolData', () => {
const mockCommandsManager = {
runCommand: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return undefined when event is null', () => {
const result = findNearbyToolData(mockCommandsManager, null);
expect(result).toBeUndefined();
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
});
it('should return undefined when event is undefined', () => {
const result = findNearbyToolData(mockCommandsManager, undefined);
expect(result).toBeUndefined();
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
});
it('should return undefined when event has no detail', () => {
const event = {};
const result = findNearbyToolData(mockCommandsManager, event);
expect(result).toBeUndefined();
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
});
it('should return undefined when event detail is null', () => {
const event = { detail: null };
const result = findNearbyToolData(mockCommandsManager, event);
expect(result).toBeUndefined();
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
});
it('should call runCommand with correct parameters when event has valid detail', () => {
const mockElement = document.createElement('div');
const mockCurrentPoints = { canvas: { x: 100, y: 200 } };
const mockToolData = { id: 'annotation-1' };
const event = {
detail: {
element: mockElement,
currentPoints: mockCurrentPoints,
},
};
mockCommandsManager.runCommand.mockReturnValue(mockToolData);
const result = findNearbyToolData(mockCommandsManager, event);
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
'getNearbyAnnotation',
{
element: mockElement,
canvasCoordinates: mockCurrentPoints.canvas,
},
'CORNERSTONE'
);
expect(result).toBe(mockToolData);
});
it('should handle event with element but no currentPoints', () => {
const mockElement = document.createElement('div');
const event = {
detail: {
element: mockElement,
},
};
findNearbyToolData(mockCommandsManager, event);
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
'getNearbyAnnotation',
{
element: mockElement,
canvasCoordinates: undefined,
},
'CORNERSTONE'
);
});
it('should handle event with currentPoints but no canvas coordinates', () => {
const mockElement = document.createElement('div');
const mockCurrentPoints = {};
const event = {
detail: {
element: mockElement,
currentPoints: mockCurrentPoints,
},
};
findNearbyToolData(mockCommandsManager, event);
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
'getNearbyAnnotation',
{
element: mockElement,
canvasCoordinates: undefined,
},
'CORNERSTONE'
);
});
it('should return result from runCommand', () => {
const mockToolData = { id: 'test-annotation', data: 'test-data' };
const event = {
detail: {
element: document.createElement('div'),
currentPoints: { canvas: { x: 50, y: 75 } },
},
};
mockCommandsManager.runCommand.mockReturnValue(mockToolData);
const result = findNearbyToolData(mockCommandsManager, event);
expect(result).toBe(mockToolData);
});
});
@@ -0,0 +1,319 @@
import { generateSegmentationCSVReport } from './generateSegmentationCSVReport';
// Mock DOM APIs
Object.defineProperty(global, 'Blob', {
writable: true,
value: jest.fn().mockImplementation((content, options) => ({
content,
options,
})),
});
Object.defineProperty(global, 'URL', {
writable: true,
value: {
createObjectURL: jest.fn().mockReturnValue('mock-url'),
},
});
const mockDocument = {
addEventListener: jest.fn(),
createElement: jest.fn().mockReturnValue({
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
}),
body: {
appendChild: jest.fn(),
removeChild: jest.fn(),
},
};
Object.defineProperty(global, 'document', {
writable: true,
value: mockDocument,
});
describe('generateSegmentationCSVReport', () => {
const mockInfo = {
reference: {
SeriesNumber: '1',
SeriesInstanceUID: 'series-uid-123',
StudyInstanceUID: 'study-uid-456',
SeriesDate: '20231201',
SeriesTime: '120000',
SeriesDescription: 'Test Series',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should generate CSV with basic segmentation data', () => {
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(global.Blob).toHaveBeenCalledWith([expect.stringContaining('Segmentation ID,seg-123')], {
type: 'text/csv;charset=utf-8;',
});
});
it('should handle segmentation data without id and label', () => {
const segmentationData = {};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(global.Blob).toHaveBeenCalledWith(
[expect.stringContaining('Segmentation ID,\nSegmentation Label,')],
{ type: 'text/csv;charset=utf-8;' }
);
});
it('should include reference information when provided', () => {
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(global.Blob).toHaveBeenCalledWith(
[expect.stringContaining('reference Series Number,1')],
{ type: 'text/csv;charset=utf-8;' }
);
});
it('should skip empty reference values', () => {
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
};
const infoWithEmptyValues = {
reference: {
SeriesNumber: '1',
SeriesInstanceUID: '',
StudyInstanceUID: null,
SeriesDate: undefined,
SeriesTime: '120000',
SeriesDescription: 'Test Series',
},
};
generateSegmentationCSVReport(segmentationData, infoWithEmptyValues);
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
expect(csvContent).toContain('reference Series Number,1');
expect(csvContent).toContain('reference Series Time,120000');
expect(csvContent).not.toContain('reference Series Instance UID');
expect(csvContent).not.toContain('reference Study Instance UID');
expect(csvContent).not.toContain('reference Series Date');
});
it('should handle segments with basic properties', () => {
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment 1',
locked: true,
active: false,
},
'2': {
segmentIndex: 2,
label: 'Segment 2',
locked: false,
active: true,
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
expect(csvContent).toContain('Label,Segment 1,Segment 2');
expect(csvContent).toContain('Segment Index,1,2');
expect(csvContent).toContain('Locked,Yes,No');
expect(csvContent).toContain('Active,No,Yes');
});
it('should handle segments with statistics', () => {
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment 1',
locked: false,
active: true,
cachedStats: {
namedStats: {
mean: {
name: 'mean',
label: 'Mean',
value: 100.5,
unit: 'HU',
},
volume: {
name: 'volume',
label: 'Volume',
value: 250.75,
unit: 'mm³',
},
},
},
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
expect(csvContent).toContain('Mean (HU),100.5');
expect(csvContent).toContain('Volume (mm³),250.75');
});
it('should handle segments with statistics without units', () => {
const segmentationData = {
segmentationId: 'seg-123',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment 1',
cachedStats: {
namedStats: {
count: {
name: 'count',
label: 'Count',
value: 42,
},
},
},
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
expect(csvContent).toContain('Count,42');
});
it('should handle segments without cachedStats', () => {
const segmentationData = {
segmentationId: 'seg-123',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment 1',
locked: false,
active: true,
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(global.Blob).toHaveBeenCalled();
});
it('should handle segments with empty namedStats', () => {
const segmentationData = {
segmentationId: 'seg-123',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment 1',
cachedStats: {
namedStats: {},
},
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(global.Blob).toHaveBeenCalled();
});
it('should escape CSV special characters', () => {
const segmentationData = {
segmentationId: 'seg,with,commas',
label: 'Test "quoted" label',
segments: {
'1': {
segmentIndex: 1,
label: 'Segment\nwith\nnewlines',
cachedStats: {
namedStats: {
test: {
name: 'test',
label: 'Test,Value',
value: 'value"with"quotes',
},
},
},
},
},
};
generateSegmentationCSVReport(segmentationData, mockInfo);
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
expect(csvContent).toContain('"seg,with,commas"');
expect(csvContent).toContain('"Test ""quoted"" label"');
expect(csvContent).toContain('"Segment\nwith\nnewlines"');
});
it('should create download link with correct attributes', () => {
const mockLink = {
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
};
mockDocument.createElement.mockReturnValue(mockLink);
const segmentationData = {
segmentationId: 'seg-123',
label: 'Test Segmentation',
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(mockLink.setAttribute).toHaveBeenCalledWith('href', 'mock-url');
expect(mockLink.setAttribute).toHaveBeenCalledWith(
'download',
expect.stringMatching(/Test Segmentation_Report_\d{4}-\d{2}-\d{2}\.csv/)
);
expect(mockLink.click).toHaveBeenCalled();
expect(mockDocument.body.appendChild).toHaveBeenCalledWith(mockLink);
expect(mockDocument.body.removeChild).toHaveBeenCalledWith(mockLink);
});
it('should use default filename when label is missing', () => {
const mockLink = {
setAttribute: jest.fn(),
click: jest.fn(),
style: {},
};
mockDocument.createElement.mockReturnValue(mockLink);
const segmentationData = {
segmentationId: 'seg-123',
};
generateSegmentationCSVReport(segmentationData, mockInfo);
expect(mockLink.setAttribute).toHaveBeenCalledWith(
'download',
expect.stringMatching(/Segmentation_Report_\d{4}-\d{2}-\d{2}\.csv/)
);
});
});
@@ -0,0 +1,70 @@
import { getViewportEnabledElement } from './getViewportEnabledElement';
import getActiveViewportEnabledElement from './getActiveViewportEnabledElement';
jest.mock('./getViewportEnabledElement', () => ({
getViewportEnabledElement: jest.fn(),
}));
describe('getActiveViewportEnabledElement', () => {
const mockViewportGridService = {
getState: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return enabled element for active viewport', () => {
const mockEnabledElement = { viewport: 'test-viewport' };
mockViewportGridService.getState.mockReturnValue({ activeViewportId: 'viewport-1' });
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getActiveViewportEnabledElement(mockViewportGridService);
expect(mockViewportGridService.getState).toHaveBeenCalledTimes(1);
expect(getViewportEnabledElement).toHaveBeenCalledWith('viewport-1');
expect(result).toBe(mockEnabledElement);
});
it('should handle null activeViewportId', () => {
const mockEnabledElement = null;
mockViewportGridService.getState.mockReturnValue({ activeViewportId: null });
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getActiveViewportEnabledElement(mockViewportGridService);
expect(getViewportEnabledElement).toHaveBeenCalledWith(null);
expect(result).toBe(null);
});
it('should handle undefined activeViewportId', () => {
const mockEnabledElement = undefined;
mockViewportGridService.getState.mockReturnValue({ activeViewportId: undefined });
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getActiveViewportEnabledElement(mockViewportGridService);
expect(getViewportEnabledElement).toHaveBeenCalledWith(undefined);
expect(result).toBe(undefined);
});
it('should handle empty state object', () => {
const mockEnabledElement = undefined;
mockViewportGridService.getState.mockReturnValue({});
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getActiveViewportEnabledElement(mockViewportGridService);
expect(getViewportEnabledElement).toHaveBeenCalledWith(undefined);
expect(result).toBe(undefined);
});
it('should handle getViewportEnabledElement returning null', () => {
mockViewportGridService.getState.mockReturnValue({ activeViewportId: 'viewport-1' });
(getViewportEnabledElement as jest.Mock).mockReturnValue(null);
const result = getActiveViewportEnabledElement(mockViewportGridService);
expect(result).toBe(null);
});
});
@@ -0,0 +1,313 @@
import { getCenterExtent } from './getCenterExtent';
describe('getCenterExtent', () => {
it('should return default values when points is undefined', () => {
const measurement = {};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 0],
extent: {
min: [0, 0, 0],
max: [0, 0, 0],
},
});
});
it('should return default values when points is null', () => {
const measurement = { points: null };
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 0],
extent: {
min: [0, 0, 0],
max: [0, 0, 0],
},
});
});
it('should return default values when points is not an array', () => {
const measurement = { points: 'invalid' };
// @ts-expect-error - purposely passing an invalid type
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 0],
extent: {
min: [0, 0, 0],
max: [0, 0, 0],
},
});
});
it('should return default values when points array is empty', () => {
const measurement = { points: [] };
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 0],
extent: {
min: [0, 0, 0],
max: [0, 0, 0],
},
});
});
it('should handle single point correctly', () => {
const measurement = {
points: [[5, 10, 15]],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [5, 10, 15],
extent: {
min: [5, 10, 15],
max: [5, 10, 15],
},
});
});
it('should calculate center and extent for two points', () => {
const measurement = {
points: [
[0, 0, 0],
[10, 20, 30],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [5, 10, 15],
extent: {
min: [0, 0, 0],
max: [10, 20, 30],
},
});
});
it('should calculate center and extent for multiple points', () => {
const measurement = {
points: [
[0, 0, 0],
[10, 20, 30],
[5, 5, 5],
[-5, 15, 25],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [2.5, 10, 15],
extent: {
min: [-5, 0, 0],
max: [10, 20, 30],
},
});
});
it('should handle negative coordinates', () => {
const measurement = {
points: [
[-10, -20, -30],
[-5, -15, -25],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [-7.5, -17.5, -27.5],
extent: {
min: [-10, -20, -30],
max: [-5, -15, -25],
},
});
});
it('should handle mixed positive and negative coordinates', () => {
const measurement = {
points: [
[-10, -5, 0],
[10, 5, 20],
[0, 0, -10],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 5],
extent: {
min: [-10, -5, -10],
max: [10, 5, 20],
},
});
});
it('should handle floating point coordinates', () => {
const measurement = {
points: [
[1.5, 2.3, 3.1],
[4.2, 8.9, 6.3],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [2.85, 5.6, 4.7],
extent: {
min: [1.5, 2.3, 3.1],
max: [4.2, 8.9, 6.3],
},
});
});
it('should handle points with same coordinates', () => {
const measurement = {
points: [
[5, 5, 5],
[5, 5, 5],
[5, 5, 5],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [5, 5, 5],
extent: {
min: [5, 5, 5],
max: [5, 5, 5],
},
});
});
it('should handle zero coordinates', () => {
const measurement = {
points: [
[0, 0, 0],
[0, 0, 0],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0, 0, 0],
extent: {
min: [0, 0, 0],
max: [0, 0, 0],
},
});
});
it('should handle large coordinate values', () => {
const measurement = {
points: [
[1000000, 2000000, 3000000],
[1000001, 2000001, 3000001],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [1000000.5, 2000000.5, 3000000.5],
extent: {
min: [1000000, 2000000, 3000000],
max: [1000001, 2000001, 3000001],
},
});
});
it('should handle points in different order', () => {
const measurement = {
points: [
[10, 20, 30],
[0, 0, 0],
[5, 10, 15],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [5, 10, 15],
extent: {
min: [0, 0, 0],
max: [10, 20, 30],
},
});
});
it('should handle points where min and max are not the first point', () => {
const measurement = {
points: [
[5, 5, 5],
[0, 0, 0],
[10, 10, 10],
[3, 3, 3],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [5, 5, 5],
extent: {
min: [0, 0, 0],
max: [10, 10, 10],
},
});
});
it('should handle asymmetric extent distribution', () => {
const measurement = {
points: [
[0, 0, 0],
[100, 10, 1],
[50, 50, 50],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [50, 25, 25],
extent: {
min: [0, 0, 0],
max: [100, 50, 50],
},
});
});
it('should handle points with different ranges per dimension', () => {
const measurement = {
points: [
[0, 100, 1000],
[1, 101, 1001],
[0.5, 100.5, 1000.5],
],
};
const result = getCenterExtent(measurement);
expect(result).toEqual({
center: [0.5, 100.5, 1000.5],
extent: {
min: [0, 100, 1000],
max: [1, 101, 1001],
},
});
});
});
@@ -0,0 +1,59 @@
import { Enums } from '@cornerstonejs/core';
import getCornerstoneBlendMode from './getCornerstoneBlendMode';
jest.mock('@cornerstonejs/core', () => ({
Enums: {
BlendModes: {
COMPOSITE: 'composite',
MAXIMUM_INTENSITY_BLEND: 'mip',
MINIMUM_INTENSITY_BLEND: 'minip',
AVERAGE_INTENSITY_BLEND: 'avg',
},
},
}));
describe('getCornerstoneBlendMode', () => {
it('should return COMPOSITE when blendMode is null', () => {
const result = getCornerstoneBlendMode(null);
expect(result).toBe(Enums.BlendModes.COMPOSITE);
});
it('should return COMPOSITE when blendMode is undefined', () => {
const result = getCornerstoneBlendMode(undefined);
expect(result).toBe(Enums.BlendModes.COMPOSITE);
});
it('should return COMPOSITE when blendMode is empty string', () => {
const result = getCornerstoneBlendMode('');
expect(result).toBe(Enums.BlendModes.COMPOSITE);
});
it('should return MAXIMUM_INTENSITY_BLEND when blendMode is mip', () => {
const result = getCornerstoneBlendMode('mip');
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
});
it('should return MAXIMUM_INTENSITY_BLEND when blendMode is MIP (uppercase)', () => {
const result = getCornerstoneBlendMode('MIP');
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
});
it('should return MINIMUM_INTENSITY_BLEND when blendMode is MINIP (uppercase)', () => {
const result = getCornerstoneBlendMode('MINIP');
expect(result).toBe(Enums.BlendModes.MINIMUM_INTENSITY_BLEND);
});
it('should return AVERAGE_INTENSITY_BLEND when blendMode is avg', () => {
const result = getCornerstoneBlendMode('avg');
expect(result).toBe(Enums.BlendModes.AVERAGE_INTENSITY_BLEND);
});
it('should throw error for unsupported blend mode', () => {
expect(() => getCornerstoneBlendMode('invalid')).toThrow('Unsupported blend mode: invalid');
});
it('should handle mixed case blend mode', () => {
const result = getCornerstoneBlendMode('MiP');
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
});
});
@@ -0,0 +1,60 @@
import { Enums } from '@cornerstonejs/core';
import getCornerstoneOrientation from './getCornerstoneOrientation';
jest.mock('@cornerstonejs/core', () => ({
Enums: {
OrientationAxis: {
AXIAL: 'axial',
SAGITTAL: 'sagittal',
CORONAL: 'coronal',
ACQUISITION: 'acquisition',
},
},
}));
describe('getCornerstoneOrientation', () => {
it('should return AXIAL when orientation is axial', () => {
const result = getCornerstoneOrientation('axial');
expect(result).toBe(Enums.OrientationAxis.AXIAL);
});
it('should return AXIAL when orientation is AXIAL (uppercase)', () => {
const result = getCornerstoneOrientation('AXIAL');
expect(result).toBe(Enums.OrientationAxis.AXIAL);
});
it('should return SAGITTAL when orientation is sagittal', () => {
const result = getCornerstoneOrientation('sagittal');
expect(result).toBe(Enums.OrientationAxis.SAGITTAL);
});
it('should return CORONAL when orientation is coronal', () => {
const result = getCornerstoneOrientation('coronal');
expect(result).toBe(Enums.OrientationAxis.CORONAL);
});
it('should return ACQUISITION for unknown orientation', () => {
const result = getCornerstoneOrientation('unknown');
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
});
it('should return ACQUISITION when orientation is null', () => {
const result = getCornerstoneOrientation(null);
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
});
it('should return ACQUISITION when orientation is undefined', () => {
const result = getCornerstoneOrientation(undefined);
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
});
it('should return ACQUISITION when orientation is empty string', () => {
const result = getCornerstoneOrientation('');
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
});
it('should handle mixed case orientation', () => {
const result = getCornerstoneOrientation('CoRoNaL');
expect(result).toBe(Enums.OrientationAxis.CORONAL);
});
});
@@ -0,0 +1,91 @@
import type { Types } from '@ohif/core';
import { Enums } from '@cornerstonejs/core';
import getCornerstoneViewportType from './getCornerstoneViewportType';
jest.mock('@cornerstonejs/core', () => ({
Enums: {
ViewportType: {
STACK: 'stack',
VIDEO: 'video',
WHOLE_SLIDE: 'wholeslide',
ORTHOGRAPHIC: 'orthographic',
VOLUME_3D: 'volume3d',
},
},
}));
describe('getCornerstoneViewportType', () => {
it('should return STACK when viewportType is stack', () => {
const result = getCornerstoneViewportType('stack');
expect(result).toBe(Enums.ViewportType.STACK);
});
it('should return STACK when viewportType is STACK (uppercase)', () => {
const result = getCornerstoneViewportType('STACK');
expect(result).toBe(Enums.ViewportType.STACK);
});
it('should return VIDEO when viewportType is video', () => {
const result = getCornerstoneViewportType('video');
expect(result).toBe(Enums.ViewportType.VIDEO);
});
it('should return WHOLE_SLIDE when viewportType is wholeslide', () => {
const result = getCornerstoneViewportType('wholeslide');
expect(result).toBe(Enums.ViewportType.WHOLE_SLIDE);
});
it('should return ORTHOGRAPHIC when viewportType is volume', () => {
const result = getCornerstoneViewportType('volume');
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
});
it('should return ORTHOGRAPHIC when viewportType is orthographic', () => {
const result = getCornerstoneViewportType('orthographic');
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
});
it('should return VOLUME_3D when viewportType is volume3d', () => {
const result = getCornerstoneViewportType('volume3d');
expect(result).toBe(Enums.ViewportType.VOLUME_3D);
});
it('should throw error for invalid viewport type', () => {
expect(() => getCornerstoneViewportType('invalid')).toThrow(
'Invalid viewport type: invalid. Valid types are: stack, volume, video, wholeslide'
);
});
it('should use displaySet viewportType when provided', () => {
const displaySets = [{ viewportType: 'stack' }] as Types.DisplaySet[];
const result = getCornerstoneViewportType('volume', displaySets);
expect(result).toBe(Enums.ViewportType.STACK);
});
it('should use displaySet viewportType with case insensitive matching', () => {
const displaySets = [{ viewportType: 'VIDEO' }] as Types.DisplaySet[];
const result = getCornerstoneViewportType('stack', displaySets);
expect(result).toBe(Enums.ViewportType.VIDEO);
});
it('should fallback to viewportType when displaySet has no viewportType', () => {
const displaySets = [{}] as Types.DisplaySet[];
const result = getCornerstoneViewportType('volume', displaySets);
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
});
it('should handle empty displaySets array', () => {
const result = getCornerstoneViewportType('stack', []);
expect(result).toBe(Enums.ViewportType.STACK);
});
it('should handle null displaySets', () => {
const result = getCornerstoneViewportType('video', null);
expect(result).toBe(Enums.ViewportType.VIDEO);
});
it('should handle undefined displaySets', () => {
const result = getCornerstoneViewportType('wholeslide', undefined);
expect(result).toBe(Enums.ViewportType.WHOLE_SLIDE);
});
});
@@ -0,0 +1,85 @@
import getInterleavedFrames from './getInterleavedFrames';
describe('getInterleavedFrames', () => {
it('should return single element when input has one element', () => {
const imageIds = ['image-1'];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([{ imageId: 'image-1', imageIdIndex: 0 }]);
});
it('should return correct order for three elements', () => {
const imageIds = ['image-1', 'image-2', 'image-3'];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([
{ imageId: 'image-2', imageIdIndex: 1 },
{ imageId: 'image-1', imageIdIndex: 0 },
{ imageId: 'image-3', imageIdIndex: 2 },
]);
});
it('should start with middle element', () => {
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5'];
const result = getInterleavedFrames(imageIds);
expect(result[0]).toEqual({ imageId: 'image-3', imageIdIndex: 2 });
});
it('should interleave elements correctly for even length array', () => {
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5', 'image-6'];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([
{ imageId: 'image-4', imageIdIndex: 3 },
{ imageId: 'image-3', imageIdIndex: 2 },
{ imageId: 'image-5', imageIdIndex: 4 },
{ imageId: 'image-2', imageIdIndex: 1 },
{ imageId: 'image-6', imageIdIndex: 5 },
{ imageId: 'image-1', imageIdIndex: 0 },
]);
});
it('should interleave elements correctly for odd length array', () => {
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5', 'image-6', 'image-7'];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([
{ imageId: 'image-4', imageIdIndex: 3 },
{ imageId: 'image-3', imageIdIndex: 2 },
{ imageId: 'image-5', imageIdIndex: 4 },
{ imageId: 'image-2', imageIdIndex: 1 },
{ imageId: 'image-6', imageIdIndex: 5 },
{ imageId: 'image-1', imageIdIndex: 0 },
{ imageId: 'image-7', imageIdIndex: 6 },
]);
});
it('should handle large array correctly', () => {
const imageIds = Array.from({ length: 10 }, (_, i) => `image-${i + 1}`);
const result = getInterleavedFrames(imageIds);
expect(result).toHaveLength(10);
expect(result[0]).toEqual({ imageId: 'image-6', imageIdIndex: 5 });
expect(result[8]).toEqual({ imageId: 'image-10', imageIdIndex: 9 });
expect(result[9]).toEqual({ imageId: 'image-1', imageIdIndex: 0 });
});
it('should handle empty array', () => {
const imageIds = [];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([]);
});
it('should handle duplicate imageIds with different indices', () => {
const imageIds = ['duplicate', 'unique', 'duplicate'];
const result = getInterleavedFrames(imageIds);
expect(result).toEqual([
{ imageId: 'unique', imageIdIndex: 1 },
{ imageId: 'duplicate', imageIdIndex: 0 },
{ imageId: 'duplicate', imageIdIndex: 2 },
]);
});
});
@@ -1,4 +1,17 @@
export default function getInterleavedFrames(imageIds) {
interface ImageIdToPrefetch {
imageId: string;
imageIdIndex: number;
}
export default function getInterleavedFrames(imageIds: string[]): ImageIdToPrefetch[] {
if (imageIds.length === 0) {
return [];
}
if (imageIds.length === 1) {
return [{ imageId: imageIds[0], imageIdIndex: 0 }];
}
const minImageIdIndex = 0;
const maxImageIdIndex = imageIds.length - 1;
@@ -8,7 +21,7 @@ export default function getInterleavedFrames(imageIds) {
let upperImageIdIndex = middleImageIdIndex;
// Build up an array of images to prefetch, starting with the current image.
const imageIdsToPrefetch = [
const imageIdsToPrefetch: ImageIdToPrefetch[] = [
{ imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex },
];
@@ -0,0 +1,145 @@
import getNthFrames from './getNthFrames';
describe('getNthFrames', () => {
const createMockImageLoadRequest = (imageId: string, imageIdIndex: number) => ({
imageId,
callLoadImage: jest.fn(),
additionalDetails: 'test-details',
imageIdIndex,
options: { test: 'option' },
});
it('should return empty array when input is empty', () => {
const result = getNthFrames([]);
expect(result).toEqual([]);
});
it('should return same array when input has one element', () => {
const imageIds = [createMockImageLoadRequest('image-1', 0)];
const result = getNthFrames(imageIds);
expect(result).toEqual(imageIds);
});
it('should return same array when input has two elements', () => {
const imageIds = [
createMockImageLoadRequest('image-1', 0),
createMockImageLoadRequest('image-2', 1),
];
const result = getNthFrames(imageIds);
expect(result).toEqual(imageIds);
});
it('should prioritize first two elements', () => {
const imageIds = Array.from({ length: 10 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
expect(result[0]).toBe(imageIds[0]);
expect(result[1]).toBe(imageIds[1]);
});
it('should prioritize last three elements', () => {
const imageIds = Array.from({ length: 10 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
expect(result).toContain(imageIds[7]);
expect(result).toContain(imageIds[8]);
expect(result).toContain(imageIds[9]);
});
it('should include center elements', () => {
const imageIds = Array.from({ length: 20 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
for (let i = Math.ceil(centerStart); i < centerEnd; i++) {
if (i >= 0 && i < imageIds.length) {
expect(result).toContain(imageIds[i]);
}
}
});
it('should include nth elements where i % 7 === 2', () => {
const imageIds = Array.from({ length: 30 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
for (let i = 0; i < imageIds.length; i++) {
if (i % 7 === 2 && i >= 2 && i <= imageIds.length - 4) {
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
if (!(i > centerStart && i < centerEnd)) {
expect(result).toContain(imageIds[i]);
}
}
}
});
it('should include nth elements where i % 7 === 5', () => {
const imageIds = Array.from({ length: 30 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
for (let i = 0; i < imageIds.length; i++) {
if (i % 7 === 5 && i >= 2 && i <= imageIds.length - 4) {
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
if (!(i > centerStart && i < centerEnd)) {
expect(result).toContain(imageIds[i]);
}
}
}
});
it('should handle large arrays correctly', () => {
const imageIds = Array.from({ length: 100 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
expect(result.length).toBe(100);
expect(result[0]).toBe(imageIds[0]);
expect(result[1]).toBe(imageIds[1]);
expect(result).toContain(imageIds[97]);
expect(result).toContain(imageIds[98]);
expect(result).toContain(imageIds[99]);
});
it('should handle arrays where centerStart is negative', () => {
const imageIds = Array.from({ length: 3 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
expect(result).toEqual(imageIds);
});
it('should handle arrays where centerEnd exceeds length', () => {
const imageIds = Array.from({ length: 4 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
expect(result).toEqual(expect.arrayContaining(imageIds));
expect(result.length).toBe(4);
});
it('should preserve object references', () => {
const imageIds = Array.from({ length: 10 }, (_, i) =>
createMockImageLoadRequest(`image-${i}`, i)
);
const result = getNthFrames(imageIds);
result.forEach(item => {
expect(imageIds).toContain(item);
});
});
});
@@ -9,12 +9,9 @@
* What this does is return the first/center/start objects, as those
* are often used first, then a selection of objects scattered over the
* instances in order to allow making requests over a set of image instances.
*
* @param {[]} imageIds
* @returns [] reordered to be an nth selection
*/
export default function getNthFrames(imageIds) {
const frames = [[], [], [], [], []];
export default function getNthFrames<T>(imageIds: T[]): T[] {
const frames: T[][] = [[], [], [], [], []];
const centerStart = imageIds.length / 2 - 3;
const centerEnd = centerStart + 6;
@@ -0,0 +1,83 @@
import { getEnabledElement } from '@cornerstonejs/core';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
import { getViewportEnabledElement } from './getViewportEnabledElement';
jest.mock('@cornerstonejs/core', () => ({
getEnabledElement: jest.fn(),
}));
jest.mock('../state', () => ({
getEnabledElement: jest.fn(),
}));
describe('getViewportEnabledElement', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return enabled element when OHIF getEnabledElement returns element', () => {
const mockElement = document.createElement('div');
const mockEnabledElement = { viewport: 'test-viewport' };
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getViewportEnabledElement('test-viewport-id');
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
expect(result).toBe(mockEnabledElement);
});
it('should return enabled element when OHIF getEnabledElement returns null', () => {
const mockEnabledElement = { viewport: 'test-viewport' };
(OHIFgetEnabledElement as jest.Mock).mockReturnValue(null);
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getViewportEnabledElement('test-viewport-id');
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
expect(getEnabledElement).toHaveBeenCalledWith(undefined);
expect(result).toBe(mockEnabledElement);
});
it('should return enabled element when OHIF getEnabledElement returns object without element', () => {
const mockEnabledElement = { viewport: 'test-viewport' };
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({});
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
const result = getViewportEnabledElement('test-viewport-id');
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
expect(getEnabledElement).toHaveBeenCalledWith(undefined);
expect(result).toBe(mockEnabledElement);
});
it('should return null when cornerstone getEnabledElement returns null', () => {
const mockElement = document.createElement('div');
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
(getEnabledElement as jest.Mock).mockReturnValue(null);
const result = getViewportEnabledElement('test-viewport-id');
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
expect(result).toBe(null);
});
it('should return undefined when cornerstone getEnabledElement returns undefined', () => {
const mockElement = document.createElement('div');
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
(getEnabledElement as jest.Mock).mockReturnValue(undefined);
const result = getViewportEnabledElement('test-viewport-id');
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
expect(result).toBe(undefined);
});
});
@@ -0,0 +1,50 @@
import { getViewportOrientationFromImageOrientationPatient } from './getViewportOrientationFromImageOrientationPatient';
describe('getViewportOrientationFromImageOrientationPatient', () => {
it('should return undefined when imageOrientationPatient is null', () => {
const result = getViewportOrientationFromImageOrientationPatient(null);
expect(result).toBeUndefined();
});
it('should return undefined when imageOrientationPatient is undefined', () => {
const result = getViewportOrientationFromImageOrientationPatient(undefined);
expect(result).toBeUndefined();
});
it('should return undefined when imageOrientationPatient has length less than 6', () => {
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1]);
expect(result).toBeUndefined();
});
it('should return undefined when imageOrientationPatient has length greater than 6', () => {
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1, 0, 0]);
expect(result).toBeUndefined();
});
it('should return undefined when imageOrientationPatient is empty array', () => {
const result = getViewportOrientationFromImageOrientationPatient([]);
expect(result).toBeUndefined();
});
it('should return "axial" when imageOrientationPatient matches axial orientation', () => {
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1, 0]);
expect(result).toBe('axial');
});
it('should return "sagittal" when imageOrientationPatient matches sagittal orientation', () => {
const result = getViewportOrientationFromImageOrientationPatient([0, 1, 0, 0, 0, -1]);
expect(result).toBe('sagittal');
});
it('should return "coronal" when imageOrientationPatient matches coronal orientation', () => {
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 0, -1]);
expect(result).toBe('coronal');
});
it('should return undefined when no orientation matches', () => {
const result = getViewportOrientationFromImageOrientationPatient([
0.5, 0.5, 0.7, 0.3, 0.8, 0.5,
]);
expect(result).toBeUndefined();
});
});
@@ -0,0 +1,307 @@
import { getUpdatedViewportsForSegmentation } from './hydrationUtils';
describe('getUpdatedViewportsForSegmentation', () => {
const mockHangingProtocolService = {
getViewportsRequireUpdate: jest.fn(),
};
const mockViewportGridService = {
getState: jest.fn(),
};
const mockServicesManager = {
services: {
hangingProtocolService: mockHangingProtocolService,
viewportGridService: mockViewportGridService,
},
};
const mockViewport = {
viewportOptions: {
viewportId: 'target-viewport-id',
},
};
const mockViewports = new Map([
['viewport-1', mockViewport],
['active-viewport-id', mockViewport],
]);
const defaultParameters = {
viewportId: 'viewport-1',
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
displaySetInstanceUIDs: ['display-set-1'],
};
beforeEach(() => {
jest.clearAllMocks();
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: true,
viewports: mockViewports,
activeViewportId: 'active-viewport-id',
});
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue([]);
});
it('should get updated viewports for segmentation', () => {
const mockUpdatedViewports = [
{
viewportOptions: {
viewportType: 'stack',
},
},
{
viewportOptions: {
viewportType: 'volume',
},
},
];
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(mockViewportGridService.getState).toHaveBeenCalled();
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
defaultParameters.displaySetInstanceUIDs[0],
true
);
expect(result).toEqual(mockUpdatedViewports);
});
it('should handle viewports without viewportOptions', () => {
const mockUpdatedViewports = [
{
viewportOptions: {
viewportType: 'stack',
},
},
{
someOtherProperty: 'value',
},
{
viewportOptions: null,
},
];
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(result).toEqual([
{
viewportOptions: {
viewportType: 'stack',
},
},
{
someOtherProperty: 'value',
},
{
viewportOptions: null,
},
]);
});
it('should use activeViewportId when viewportId is not provided', () => {
const result = getUpdatedViewportsForSegmentation({
...defaultParameters,
viewportId: null,
});
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
defaultParameters.displaySetInstanceUIDs[0],
true
);
expect(result).toEqual([]);
});
it('should use activeViewportId when viewportId is undefined', () => {
const result = getUpdatedViewportsForSegmentation({
...defaultParameters,
viewportId: undefined,
});
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
defaultParameters.displaySetInstanceUIDs[0],
true
);
expect(result).toEqual([]);
});
it('should handle isHangingProtocolLayout false', () => {
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: false,
viewports: mockViewports,
activeViewportId: 'active-viewport-id',
});
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
defaultParameters.displaySetInstanceUIDs[0],
false
);
expect(result).toEqual([]);
});
it('should handle multiple displaySetInstanceUIDs by using first one', () => {
const result = getUpdatedViewportsForSegmentation({
...defaultParameters,
displaySetInstanceUIDs: ['display-set-1', 'display-set-2', 'display-set-3'],
});
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
'display-set-1',
true
);
expect(result).toEqual([]);
});
it('should handle empty displaySetInstanceUIDs array', () => {
const result = getUpdatedViewportsForSegmentation({
...defaultParameters,
displaySetInstanceUIDs: [],
});
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
mockViewport.viewportOptions.viewportId,
undefined,
true
);
expect(result).toEqual([]);
});
it('should handle viewport not found in viewports map', () => {
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: true,
viewports: new Map(),
activeViewportId: 'non-existent-viewport',
});
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
});
it('should handle viewport with missing viewportOptions', () => {
const viewportWithoutOptions = {};
const viewportsMap = new Map([['viewport-1', viewportWithoutOptions]]);
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: true,
viewports: viewportsMap,
activeViewportId: 'active-viewport-id',
});
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
});
it('should handle viewport with null viewportOptions', () => {
const viewportWithNullOptions = {
viewportOptions: null,
};
const viewportsMap = new Map([['viewport-1', viewportWithNullOptions]]);
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: true,
viewports: viewportsMap,
activeViewportId: 'active-viewport-id',
});
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
});
it('should handle getViewportsRequireUpdate returning null', () => {
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(null);
expect(getUpdatedViewportsForSegmentation(defaultParameters)).toEqual(null);
});
it('should handle mixed viewport types including volume3d', () => {
const mockUpdatedViewports = [
{ viewportOptions: { viewportType: 'stack' } },
{ viewportOptions: { viewportType: 'volume3d' } },
{ viewportOptions: { viewportType: 'volume3d' } },
{ viewportOptions: { viewportType: 'orthogonal' } },
];
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(result).toEqual(mockUpdatedViewports);
});
it('should handle viewports with undefined viewportType', () => {
const mockUpdatedViewports = [
{
viewportOptions: {
viewportType: undefined,
},
},
{
viewportOptions: {
viewportType: 'volume3d',
},
},
{
viewportOptions: {
someOtherProperty: 'value',
},
},
];
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(result).toEqual(mockUpdatedViewports);
});
it('should handle complex viewport structure', () => {
const complexViewport = {
viewportOptions: {
viewportId: 'complex-viewport-id',
viewportType: 'stack',
orientation: 'axial',
initialImageOptions: {
index: 0,
},
},
displaySetOptions: {
displaySetInstanceUID: 'display-set-1',
},
};
const viewportsMap = new Map([['viewport-1', complexViewport]]);
mockViewportGridService.getState.mockReturnValue({
isHangingProtocolLayout: true,
viewports: viewportsMap,
activeViewportId: 'active-viewport-id',
});
const mockUpdatedViewports = [
{
viewportOptions: {
viewportType: 'stack',
},
},
];
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
const result = getUpdatedViewportsForSegmentation(defaultParameters);
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
'complex-viewport-id',
defaultParameters.displaySetInstanceUIDs[0],
true
);
expect(result).toEqual(mockUpdatedViewports);
});
});
@@ -16,7 +16,7 @@ function getUpdatedViewportsForSegmentation({
isHangingProtocolLayout
);
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
return updatedViewports;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
@@ -10,6 +10,7 @@ import promptHydrationDialog, {
HydrationSRResult,
} from './promptHydrationDialog';
import { getCenterExtent } from './getCenterExtent';
import { createSegmentationForViewport } from './createSegmentationForViewport';
const utils = {
handleSegmentChange,
@@ -18,6 +19,7 @@ const utils = {
setupSegmentationModifiedHandler,
promptHydrationDialog,
getCenterExtent,
createSegmentationForViewport,
};
export type { HydrationDialogProps, HydrationCallback, HydrationSRResult };
@@ -0,0 +1,342 @@
import { log, Enums } from '@ohif/core';
import { EVENTS } from '@cornerstonejs/core';
import initViewTiming from './initViewTiming';
jest.mock('@ohif/core', () => ({
log: {
timingKeys: {},
timeEnd: jest.fn(),
},
Enums: {
TimingEnum: {
DISPLAY_SETS_TO_ALL_IMAGES: 'DISPLAY_SETS_TO_ALL_IMAGES',
DISPLAY_SETS_TO_FIRST_IMAGE: 'DISPLAY_SETS_TO_FIRST_IMAGE',
STUDY_TO_FIRST_IMAGE: 'STUDY_TO_FIRST_IMAGE',
SCRIPT_TO_VIEW: 'SCRIPT_TO_VIEW',
},
},
}));
jest.mock('@cornerstonejs/core', () => ({
EVENTS: {
IMAGE_RENDERED: 'IMAGE_RENDERED',
},
}));
describe('initViewTiming', () => {
const mockElement = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const defaultParameters = {
element: mockElement,
};
const clearUnevenlyHandledViewportState = () => {
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
imageRenderedListener({
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
});
};
beforeEach(() => {
jest.clearAllMocks();
// @ts-expect-error - restarting the object that will have values set by each test
log.timingKeys = {};
});
it('should return early when no timing keys are set', () => {
// @ts-expect-error - the idea is to test an invalid state
log.timingKeys = {};
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).not.toHaveBeenCalled();
});
it('should add event listener when timing keys are present', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).toHaveBeenCalledWith(
EVENTS.IMAGE_RENDERED,
expect.any(Function)
);
clearUnevenlyHandledViewportState();
});
it('should handle multiple timing keys', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = true;
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).toHaveBeenCalledWith(
EVENTS.IMAGE_RENDERED,
expect.any(Function)
);
clearUnevenlyHandledViewportState();
});
it('should return early in imageRenderedListener when viewportStatus is preRender', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'preRender',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).not.toHaveBeenCalled();
expect(mockElement.removeEventListener).not.toHaveBeenCalled();
clearUnevenlyHandledViewportState();
});
it('should call timeEnd for timing keys when image is rendered', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.SCRIPT_TO_VIEW);
});
it('should remove event listener after image is rendered', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(mockElement.removeEventListener).toHaveBeenCalledWith(
EVENTS.IMAGE_RENDERED,
imageRenderedListener
);
});
it('should not call timeEnd for ALL_IMAGES when viewports are still waiting', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.SCRIPT_TO_VIEW);
expect(log.timeEnd).not.toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
clearUnevenlyHandledViewportState();
});
it('should handle multiple viewports finishing in sequence', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
const mockElement2 = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
initViewTiming(defaultParameters);
initViewTiming({ element: mockElement2 });
const imageRenderedListener1 = mockElement.addEventListener.mock.calls[0][1];
const imageRenderedListener2 = mockElement2.addEventListener.mock.calls[0][1];
const evt1 = {
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
};
const evt2 = {
detail: {
viewportStatus: 'rendered',
element: mockElement2,
},
};
imageRenderedListener1(evt1);
expect(log.timeEnd).not.toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
imageRenderedListener2(evt2);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
});
it('should call timeEnd for ALL_IMAGES when last viewport finishes', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'rendered',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
});
it('should handle different viewportStatus values', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
viewportStatus: 'loading',
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
expect(mockElement.removeEventListener).toHaveBeenCalled();
});
it('should handle undefined viewportStatus', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {
detail: {
element: mockElement,
},
};
imageRenderedListener(evt);
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
expect(mockElement.removeEventListener).toHaveBeenCalled();
});
it('should handle missing detail object', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
const evt = {};
expect(() => imageRenderedListener(evt)).toThrow();
});
it('should handle timing keys with falsy values', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = false;
log.timingKeys[Enums.TimingEnum.STUDY_TO_FIRST_IMAGE] = null;
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = undefined;
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).not.toHaveBeenCalled();
});
it('should handle timing keys with truthy values', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = 'some value';
log.timingKeys[Enums.TimingEnum.STUDY_TO_FIRST_IMAGE] = 1;
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = {};
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).toHaveBeenCalledWith(
EVENTS.IMAGE_RENDERED,
expect.any(Function)
);
});
it('should initialize IMAGE_TIMING_KEYS on first call', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).toHaveBeenCalled();
});
it('should handle empty element object', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
const emptyElement = {};
expect(() => initViewTiming({ element: emptyElement })).toThrow();
});
it('should handle null element', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
expect(() => initViewTiming({ element: null })).toThrow();
});
it('should handle multiple calls with same element', () => {
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
initViewTiming(defaultParameters);
initViewTiming(defaultParameters);
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).toHaveBeenCalledTimes(3);
});
it('should handle case where no timing keys match', () => {
// @ts-expect-error - the idea is to test an invalid state
log.timingKeys = {
someOtherKey: true,
anotherKey: 'value',
};
initViewTiming(defaultParameters);
expect(mockElement.addEventListener).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,381 @@
import { eventTarget, EVENTS } from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import { initializeWebWorkerProgressHandler } from './initWebWorkerProgressHandler';
jest.mock('@cornerstonejs/core', () => ({
eventTarget: {
addEventListener: jest.fn(),
},
EVENTS: {
WEB_WORKER_PROGRESS: 'WEB_WORKER_PROGRESS',
},
}));
jest.mock('@cornerstonejs/tools', () => ({
Enums: {
WorkerTypes: {
COMPUTE_STATISTICS: 'COMPUTE_STATISTICS',
},
},
}));
describe('initializeWebWorkerProgressHandler', () => {
const mockUINotificationService = {
show: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'error').mockImplementation();
jest.spyOn(console, 'debug').mockImplementation();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should register event listener for WEB_WORKER_PROGRESS', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
expect(eventTarget.addEventListener).toHaveBeenCalledWith(
EVENTS.WEB_WORKER_PROGRESS,
expect.any(Function)
);
});
it('should skip notifications for COMPUTE_STATISTICS worker type', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: cornerstoneTools.Enums.WorkerTypes.COMPUTE_STATISTICS,
id: 'test-id',
};
eventHandler({ detail });
expect(mockUINotificationService.show).not.toHaveBeenCalled();
});
it('should show notification when progress is 0 for new task', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
eventHandler({ detail });
expect(mockUINotificationService.show).toHaveBeenCalledWith({
id: 'worker-task-test_worker',
title: 'TEST_WORKER',
message: 'Computing...',
autoClose: false,
allowDuplicates: false,
deduplicationInterval: 60000,
promise: expect.any(Promise),
promiseMessages: {
loading: 'Computing...',
success: 'Completed successfully',
error: 'Web Worker failed',
},
});
});
it('should not show duplicate notification for same task type', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id-1',
};
eventHandler({ detail });
eventHandler({ detail: { ...detail, id: 'test-id-2' } });
expect(mockUINotificationService.show).toHaveBeenCalledTimes(1);
expect(console.debug).toHaveBeenCalledWith(
'Already tracking a "TEST_WORKER" task, skipping duplicate notification'
);
});
it('should resolve promise when progress is 100', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const startDetail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
eventHandler({ detail: startDetail });
const completeDetail = {
progress: 100,
type: 'TEST_WORKER',
id: 'test-id',
};
eventHandler({ detail: completeDetail });
expect(console.debug).toHaveBeenCalledWith('Worker task "TEST_WORKER" completed successfully');
});
it('should handle completion for non-tracked task gracefully', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 100,
type: 'UNKNOWN_WORKER',
id: 'test-id',
};
eventHandler({ detail });
expect(console.debug).not.toHaveBeenCalledWith(
'Worker task "UNKNOWN_WORKER" completed successfully'
);
});
it('should normalize task keys correctly', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'Test Worker Type',
id: 'test-id',
};
eventHandler({ detail });
expect(mockUINotificationService.show).toHaveBeenCalledWith(
expect.objectContaining({
id: 'worker-task-test-worker-type',
})
);
});
it('should handle error when setting active worker task', () => {
jest.spyOn(global, 'Promise').mockImplementationOnce(() => {
throw new Error('Promise creation failed');
});
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
expect(() => eventHandler({ detail })).not.toThrow();
expect(console.error).toHaveBeenCalledWith(
'Error in web worker progress handler for type "TEST_WORKER":',
expect.any(Error)
);
jest.restoreAllMocks();
});
it('should handle error when showing notification', () => {
mockUINotificationService.show.mockImplementationOnce(() => {
throw new Error('Notification service failed');
});
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
expect(() => eventHandler({ detail })).not.toThrow();
expect(console.error).toHaveBeenCalledWith(
'Error showing web worker notification for type "TEST_WORKER":',
expect.any(Error)
);
});
it('should handle missing detail object', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
expect(() => eventHandler({})).not.toThrow();
expect(console.error).toHaveBeenCalledWith(
'Error in web worker progress handler for type "undefined":',
expect.any(Error)
);
});
it('should handle undefined detail', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
expect(() => eventHandler({ detail: undefined })).not.toThrow();
expect(console.error).toHaveBeenCalledWith(
'Error in web worker progress handler for type "undefined":',
expect.any(Error)
);
});
it('should handle cleanup error gracefully', () => {
const mockMap = new Map();
mockMap.delete = jest.fn(() => {
throw new Error('Delete failed');
});
jest.spyOn(global, 'Map').mockImplementationOnce(() => mockMap);
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
mockUINotificationService.show.mockImplementationOnce(() => {
throw new Error('Notification failed');
});
expect(() => eventHandler({ detail })).not.toThrow();
expect(console.error).toHaveBeenCalledWith(
'Error cleaning up active worker task for type "TEST_WORKER":',
expect.any(Error)
);
jest.restoreAllMocks();
});
it('should handle intermediate progress values', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const startDetail = {
progress: 0,
type: 'TEST_WORKER',
id: 'test-id',
};
eventHandler({ detail: startDetail });
const intermediateDetail = {
progress: 50,
type: 'TEST_WORKER',
id: 'test-id',
};
eventHandler({ detail: intermediateDetail });
expect(mockUINotificationService.show).toHaveBeenCalledTimes(1);
});
it('should handle multiple different worker types simultaneously', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail1 = {
progress: 0,
type: 'WORKER_TYPE_1',
id: 'test-id-1',
};
const detail2 = {
progress: 0,
type: 'WORKER_TYPE_2',
id: 'test-id-2',
};
eventHandler({ detail: detail1 });
eventHandler({ detail: detail2 });
expect(mockUINotificationService.show).toHaveBeenCalledTimes(2);
expect(mockUINotificationService.show).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
id: 'worker-task-worker_type_1',
title: 'WORKER_TYPE_1',
})
);
expect(mockUINotificationService.show).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
id: 'worker-task-worker_type_2',
title: 'WORKER_TYPE_2',
})
);
});
it('should complete multiple worker types independently', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
eventHandler({ detail: { progress: 0, type: 'WORKER_1', id: 'id-1' } });
eventHandler({ detail: { progress: 0, type: 'WORKER_2', id: 'id-2' } });
eventHandler({ detail: { progress: 100, type: 'WORKER_1', id: 'id-1' } });
expect(console.debug).toHaveBeenCalledWith('Worker task "WORKER_1" completed successfully');
eventHandler({ detail: { progress: 100, type: 'WORKER_2', id: 'id-2' } });
expect(console.debug).toHaveBeenCalledWith('Worker task "WORKER_2" completed successfully');
});
it('should handle special characters in worker type names', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: 'Test-Worker_Type@123',
id: 'test-id',
};
eventHandler({ detail });
expect(mockUINotificationService.show).toHaveBeenCalledWith(
expect.objectContaining({
id: 'worker-task-test-worker_type@123',
})
);
});
it('should handle empty worker type', () => {
initializeWebWorkerProgressHandler(mockUINotificationService);
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
const detail = {
progress: 0,
type: '',
id: 'test-id',
};
eventHandler({ detail });
expect(mockUINotificationService.show).toHaveBeenCalledWith(
expect.objectContaining({
id: 'worker-task-',
title: '',
})
);
});
});
@@ -0,0 +1,304 @@
import interleave from './interleave';
describe('interleave', () => {
beforeEach(() => {
jest.spyOn(console, 'time').mockImplementation();
jest.spyOn(console, 'timeEnd').mockImplementation();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return empty array when input is null', () => {
const result = interleave(null);
expect(result).toEqual([]);
});
it('should return empty array when input is undefined', () => {
const result = interleave(undefined);
expect(result).toEqual([]);
});
it('should return empty array when input is empty array', () => {
const result = interleave([]);
expect(result).toEqual([]);
});
it('should return same array when only one list is provided', () => {
const input = [[1, 2, 3, 4]];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4]);
expect(result).toBe(input[0]);
});
it('should interleave two arrays of equal length', () => {
const input = [
[1, 3, 5],
[2, 4, 6],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
});
it('should interleave three arrays of equal length', () => {
const input = [
[1, 4, 7],
[2, 5, 8],
[3, 6, 9],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it('should handle arrays of different lengths', () => {
const input = [
[1, 4],
[2, 5, 7, 8],
[3, 6],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
});
it('should handle first array being longer', () => {
const input = [
[1, 3, 5, 7, 9, 11],
[2, 4, 6],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 9, 11]);
});
it('should handle second array being longer', () => {
const input = [
[1, 3, 5],
[2, 4, 6, 8, 10, 12],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 8, 10, 12]);
});
it('should handle empty arrays in the input', () => {
const input = [[1, 3, 5], [], [2, 4, 6]];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
});
it('should handle all empty arrays', () => {
const input = [[], [], []];
const result = interleave(input);
expect(result).toEqual([]);
});
it('should handle single element arrays', () => {
const input = [[1], [2], [3], [4]];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4]);
});
it('should handle mixed single and multi-element arrays', () => {
const input = [[1], [2, 5, 8], [3], [4, 6, 7, 9]];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 8, 7, 9]);
});
it('should handle string arrays', () => {
const input = [
['a', 'c', 'e'],
['b', 'd', 'f'],
];
const result = interleave(input);
expect(result).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
});
it('should handle object arrays', () => {
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const obj3 = { id: 3 };
const obj4 = { id: 4 };
const input = [
[obj1, obj3],
[obj2, obj4],
];
const result = interleave(input);
expect(result).toEqual([obj1, obj2, obj3, obj4]);
});
it('should handle large arrays efficiently', () => {
const array1 = Array.from({ length: 1000 }, (_, i) => i * 2);
const array2 = Array.from({ length: 1000 }, (_, i) => i * 2 + 1);
const input = [array1, array2];
const startTime = performance.now();
const result = interleave(input);
const endTime = performance.now();
expect(result).toHaveLength(2000);
expect(result[0]).toBe(0);
expect(result[1]).toBe(1);
expect(result[2]).toBe(2);
expect(result[3]).toBe(3);
expect(endTime - startTime).toBeLessThan(100);
});
it('should handle very large number of arrays', () => {
const input = Array.from({ length: 100 }, (_, i) => [i, i + 100, i + 200]);
const result = interleave(input);
expect(result).toHaveLength(300);
expect(result.slice(0, 100)).toEqual(Array.from({ length: 100 }, (_, i) => i));
});
it('should handle arrays with different data types', () => {
const input = [
[1, 'a', true],
[2, 'b', false],
[3, 'c', null],
];
const result = interleave(input);
expect(result).toEqual([1, 2, 3, 'a', 'b', 'c', true, false, null]);
});
it('should not mutate original arrays', () => {
const array1 = [1, 3, 5];
const array2 = [2, 4, 6];
const input = [array1, array2];
const originalArray1 = [...array1];
const originalArray2 = [...array2];
interleave(input);
expect(array1).toEqual(originalArray1);
expect(array2).toEqual(originalArray2);
});
it('should handle nested arrays', () => {
const input = [
[
[1, 2],
[5, 6],
],
[
[3, 4],
[7, 8],
],
];
const result = interleave(input);
expect(result).toEqual([
[1, 2],
[3, 4],
[5, 6],
[7, 8],
]);
});
it('should handle arrays with undefined and null values', () => {
const input = [
[1, undefined, 3],
[null, 2, 4],
];
const result = interleave(input);
expect(result).toEqual([1, null, undefined, 2, 3, 4]);
});
it('should handle extremely unbalanced arrays', () => {
const input = [[1], Array.from({ length: 1000 }, (_, i) => i + 2)];
const result = interleave(input);
expect(result).toHaveLength(1001);
expect(result[0]).toBe(1);
expect(result[1]).toBe(2);
expect(result[2]).toBe(3);
});
it('should handle performance with many small arrays', () => {
const input = Array.from({ length: 1000 }, (_, i) => [i]);
const startTime = performance.now();
const result = interleave(input);
const endTime = performance.now();
expect(result).toHaveLength(1000);
expect(result).toEqual(Array.from({ length: 1000 }, (_, i) => i));
expect(endTime - startTime).toBeLessThan(50);
});
it('should handle zero values correctly', () => {
const input = [
[0, 2, 4],
[1, 3, 5],
];
const result = interleave(input);
expect(result).toEqual([0, 1, 2, 3, 4, 5]);
});
it('should handle boolean arrays', () => {
const input = [
[true, false, true],
[false, true, false],
];
const result = interleave(input);
expect(result).toEqual([true, false, false, true, true, false]);
});
it('should handle single empty array with non-empty arrays', () => {
const input = [[], [1, 2, 3], [4, 5, 6]];
const result = interleave(input);
expect(result).toEqual([1, 4, 2, 5, 3, 6]);
});
it('should maintain order within each array', () => {
const input = [
[10, 20, 30, 40, 50],
[15, 25],
[12, 22, 32],
];
const result = interleave(input);
expect(result).toEqual([10, 15, 12, 20, 25, 22, 30, 32, 40, 50]);
});
});
@@ -3,28 +3,30 @@
* in the returned list, the second items are next etc.
* Does this in a O(n) fashion, and return lists[0] if there is only one list.
*
* @param {[]} lists
* @returns [] reordered to be breadth first traversal of lists
* @param lists - Array of arrays to interleave
* @returns Array reordered to be breadth first traversal of lists
*/
export default function interleave(lists) {
export default function interleave<T>(lists: T[][]): T[] {
if (!lists || !lists.length) {
return [];
}
if (lists.length === 1) {
return lists[0];
}
console.time('interleave');
const useLists = [...lists];
const ret = [];
for (let i = 0; useLists.length > 0; i++) {
for (const list of useLists) {
for (let j = 0; j < useLists.length; j++) {
const list = useLists[j];
if (i >= list.length) {
useLists.splice(useLists.indexOf(list), 1);
useLists.splice(j, 1);
j--; // Adjust index after removal to avoid iterator skipping
continue;
}
ret.push(list[i]);
}
}
console.timeEnd('interleave');
return ret;
}
@@ -0,0 +1,547 @@
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
import getInterleavedFrames from './getInterleavedFrames';
import zip from 'lodash.zip';
import compact from 'lodash.compact';
import flatten from 'lodash.flatten';
import interleaveCenterLoader from './interleaveCenterLoader';
jest.mock('@cornerstonejs/core', () => ({
cache: {
getVolume: jest.fn(),
},
imageLoadPoolManager: {
addRequest: jest.fn(),
},
Enums: {
RequestType: {
Prefetch: 'Prefetch',
},
},
}));
jest.mock('./getInterleavedFrames', () => jest.fn());
jest.mock('lodash.zip', () => jest.fn());
jest.mock('lodash.compact', () => jest.fn());
jest.mock('lodash.flatten', () => jest.fn());
describe('interleaveCenterLoader', () => {
const mockVolumeInput = {
volumeId: 'test-volume-id',
};
const mockVolume = {
metadata: {
SeriesInstanceUID: 'test-series-uid',
},
getImageLoadRequests: jest.fn(),
};
const mockImageLoadRequest = {
imageId: 'test-image-id',
callLoadImage: jest.fn(),
additionalDetails: 'test-details',
imageIdIndex: 0,
options: { test: 'option' },
};
const mockDisplaySetsInfo = [
{
displaySetInstanceUID: 'test-volume-id',
displaySetOptions: {},
},
];
const mockMatchDetails = new Map([
[
'viewport-1',
{
displaySetsInfo: mockDisplaySetsInfo,
},
],
]);
const defaultParameters = {
data: {
viewportId: 'test-viewport-id',
volumeInputArray: [mockVolumeInput],
},
displaySetsMatchDetails: {},
viewportMatchDetails: mockMatchDetails,
};
beforeEach(() => {
jest.clearAllMocks();
(cache.getVolume as jest.Mock).mockReturnValue(mockVolume);
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest]);
(getInterleavedFrames as jest.Mock).mockReturnValue([{ imageId: 'test-image-id' }]);
(zip as jest.Mock).mockImplementation((...args) => args[0]);
(compact as jest.Mock).mockImplementation(input => input?.filter(Boolean) ?? []);
(flatten as jest.Mock).mockImplementation(input => input?.flat() ?? []);
});
it('should store viewport and volume input array mapping on first call', () => {
interleaveCenterLoader(defaultParameters);
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
});
it('should return early when volume is not found', () => {
(cache.getVolume as jest.Mock).mockReturnValue(null);
const result = interleaveCenterLoader(defaultParameters);
expect(result).toBeUndefined();
expect(mockVolume.getImageLoadRequests).not.toHaveBeenCalled();
});
it('should return early when volume input array size does not match display set UIDs', () => {
const mockMatchDetailsWithDifferentSize = new Map([
[
'viewport-1',
{
displaySetsInfo: [
{ displaySetInstanceUID: 'display-set-1' },
{ displaySetInstanceUID: 'display-set-2' },
],
},
],
]);
const result = interleaveCenterLoader({
...defaultParameters,
viewportMatchDetails: mockMatchDetailsWithDifferentSize,
});
expect(result).toBeUndefined();
});
it('should process volume and create interleaved requests', () => {
const result = interleaveCenterLoader(defaultParameters);
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id']);
expect(zip).toHaveBeenCalled();
expect(compact).toHaveBeenCalled();
expect(flatten).toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should add requests to imageLoadPoolManager with correct parameters', () => {
interleaveCenterLoader(defaultParameters);
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
expect.any(Function),
Enums.RequestType.Prefetch,
mockImageLoadRequest.additionalDetails,
0
);
});
it('should bind callLoadImage with correct parameters', () => {
interleaveCenterLoader(defaultParameters);
const addRequestCall = (imageLoadPoolManager.addRequest as jest.Mock).mock.calls[0];
const boundFunction = addRequestCall[0];
boundFunction();
expect(mockImageLoadRequest.callLoadImage).toHaveBeenCalledWith(
mockImageLoadRequest.imageId,
mockImageLoadRequest.imageIdIndex,
mockImageLoadRequest.options
);
});
it('should handle volumes without image load requests', () => {
mockVolume.getImageLoadRequests.mockReturnValue([]);
const result = interleaveCenterLoader(defaultParameters);
expect(zip).toHaveBeenCalledWith();
expect(result).toBeInstanceOf(Map);
});
it('should handle volumes with empty requests array', () => {
mockVolume.getImageLoadRequests.mockReturnValue([]);
const result = interleaveCenterLoader(defaultParameters);
expect(getInterleavedFrames).not.toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should handle volumes with null first request', () => {
mockVolume.getImageLoadRequests.mockReturnValue([null]);
const result = interleaveCenterLoader(defaultParameters);
expect(getInterleavedFrames).not.toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should handle volumes with requests without imageId', () => {
const requestWithoutImageId = {
callLoadImage: jest.fn(),
additionalDetails: 'test-details',
};
mockVolume.getImageLoadRequests.mockReturnValue([requestWithoutImageId]);
const result = interleaveCenterLoader(defaultParameters);
expect(getInterleavedFrames).not.toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should handle multiple volume inputs', () => {
const mockVolumeInput2 = {
volumeId: 'test-volume-id-2',
};
const mockVolume2 = {
metadata: {
SeriesInstanceUID: 'test-series-uid-2',
},
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
};
(cache.getVolume as jest.Mock)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume2)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume2);
const mockMatchDetailsWithMultipleVolumes = new Map([
[
'viewport-1',
{
displaySetsInfo: [
{ displaySetInstanceUID: 'test-volume-id' },
{ displaySetInstanceUID: 'test-volume-id-2' },
],
},
],
]);
interleaveCenterLoader({
...defaultParameters,
data: {
...defaultParameters.data,
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
},
viewportMatchDetails: mockMatchDetailsWithMultipleVolumes,
});
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput2.volumeId);
});
it('should handle duplicate volume IDs by not adding them again', () => {
const duplicateVolumeInput = {
volumeId: mockVolumeInput.volumeId,
};
interleaveCenterLoader({
...defaultParameters,
data: {
...defaultParameters.data,
volumeInputArray: [mockVolumeInput, duplicateVolumeInput],
},
});
expect(cache.getVolume).toHaveBeenCalledTimes(3);
});
it('should clear internal maps after processing', () => {
const firstResult = interleaveCenterLoader(defaultParameters);
const secondResult = interleaveCenterLoader(defaultParameters);
expect(firstResult).toBeInstanceOf(Map);
expect(secondResult).toBeInstanceOf(Map);
});
it('should handle multiple image load requests', () => {
const mockImageLoadRequest2 = {
imageId: 'test-image-id-2',
callLoadImage: jest.fn(),
additionalDetails: 'test-details-2',
imageIdIndex: 1,
options: { test: 'option2' },
};
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
(getInterleavedFrames as jest.Mock).mockReturnValue([
{ imageId: 'test-image-id' },
{ imageId: 'test-image-id-2' },
]);
(compact as jest.Mock).mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
(flatten as jest.Mock).mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
interleaveCenterLoader(defaultParameters);
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id', 'test-image-id-2']);
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
});
it('should handle getInterleavedFrames returning empty array', () => {
(getInterleavedFrames as jest.Mock).mockReturnValue([]);
const result = interleaveCenterLoader(defaultParameters);
expect(imageLoadPoolManager.addRequest).not.toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should handle zip returning empty result', () => {
(zip as jest.Mock).mockReturnValue([]);
const result = interleaveCenterLoader(defaultParameters);
expect(compact).toHaveBeenCalledWith([]);
expect(result).toBeInstanceOf(Map);
});
it('should handle compact filtering out null values', () => {
(compact as jest.Mock).mockReturnValue([]);
const result = interleaveCenterLoader(defaultParameters);
expect(imageLoadPoolManager.addRequest).not.toHaveBeenCalled();
expect(result).toBeInstanceOf(Map);
});
it('should map imageIds to requests correctly', () => {
const mockImageLoadRequest2 = {
imageId: 'test-image-id-2',
callLoadImage: jest.fn(),
};
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
(getInterleavedFrames as jest.Mock).mockReturnValue([
{ imageId: 'test-image-id-2' },
{ imageId: 'test-image-id' },
]);
interleaveCenterLoader(defaultParameters);
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id', 'test-image-id-2']);
});
it('should return copy of viewport volume input array map', () => {
const result = interleaveCenterLoader(defaultParameters);
expect(result.has(defaultParameters.data.viewportId)).toBe(true);
expect(result.get(defaultParameters.data.viewportId)).toEqual(
defaultParameters.data.volumeInputArray
);
});
it('should handle multiple match details with different displaySets', () => {
const mockMultipleMatchDetails = new Map([
[
'viewport-1',
{
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-1' }],
},
],
[
'viewport-2',
{
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-2' }],
},
],
]);
const result = interleaveCenterLoader({
...defaultParameters,
viewportMatchDetails: mockMultipleMatchDetails,
});
expect(result).toBeUndefined();
});
it('should handle empty match details', () => {
const emptyMatchDetails = new Map();
const result = interleaveCenterLoader({
...defaultParameters,
viewportMatchDetails: emptyMatchDetails,
});
expect(result).toBeUndefined();
});
it('should handle empty volumeInputArray', () => {
const result = interleaveCenterLoader({
...defaultParameters,
data: {
...defaultParameters.data,
volumeInputArray: [],
},
});
expect(result).toBeUndefined();
});
it('should handle requests with missing properties gracefully', () => {
const incompleteRequest = {
imageId: 'test-image-id',
callLoadImage: jest.fn(),
};
mockVolume.getImageLoadRequests.mockReturnValue([incompleteRequest]);
(getInterleavedFrames as jest.Mock).mockReturnValue([{ imageId: 'test-image-id' }]);
(compact as jest.Mock).mockReturnValue([incompleteRequest]);
(flatten as jest.Mock).mockReturnValue([incompleteRequest]);
interleaveCenterLoader(defaultParameters);
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
expect.any(Function),
Enums.RequestType.Prefetch,
undefined,
0
);
});
it('should process different viewport IDs separately', () => {
const firstResult = interleaveCenterLoader({
...defaultParameters,
data: {
viewportId: 'viewport-1',
volumeInputArray: [mockVolumeInput],
},
});
const secondResult = interleaveCenterLoader({
...defaultParameters,
data: {
viewportId: 'viewport-2',
volumeInputArray: [mockVolumeInput],
},
});
expect(firstResult.has('viewport-1')).toBe(true);
expect(secondResult.has('viewport-2')).toBe(true);
});
it('should accumulate state across multiple calls before processing', () => {
const mockVolumeInput2 = {
volumeId: 'test-volume-id-2',
};
const mockVolume2 = {
metadata: {
SeriesInstanceUID: 'test-series-uid-2',
},
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
};
(cache.getVolume as jest.Mock)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume2);
const firstCall = interleaveCenterLoader({
...defaultParameters,
data: {
viewportId: 'viewport-1',
volumeInputArray: [mockVolumeInput],
},
viewportMatchDetails: new Map([
[
'viewport-1',
{
displaySetsInfo: [
{ displaySetInstanceUID: 'test-volume-id' },
{ displaySetInstanceUID: 'missing-volume-id' },
],
},
],
]),
});
expect(firstCall).toBeUndefined();
const secondCall = interleaveCenterLoader({
...defaultParameters,
data: {
viewportId: 'viewport-2',
volumeInputArray: [mockVolumeInput2],
},
viewportMatchDetails: new Map([
[
'viewport-1',
{
displaySetsInfo: [{ displaySetInstanceUID: 'test-volume-id' }],
},
],
[
'viewport-2',
{
displaySetsInfo: [{ displaySetInstanceUID: 'test-volume-id-2' }],
},
],
]),
});
expect(secondCall).toBeInstanceOf(Map);
expect(secondCall.has('viewport-1')).toBe(true);
expect(secondCall.has('viewport-2')).toBe(true);
});
it('should handle volume with undefined metadata', () => {
const mockVolumeWithoutMetadata = {
getImageLoadRequests: jest.fn().mockReturnValue([]),
};
(cache.getVolume as jest.Mock).mockReturnValue(mockVolumeWithoutMetadata);
expect(() => interleaveCenterLoader(defaultParameters)).toThrow();
});
it('should handle multiple volumes with same image requests', () => {
const mockVolume2 = {
metadata: { SeriesInstanceUID: 'test-series-uid-2' },
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
};
const mockVolumeInput2 = { volumeId: 'test-volume-id-2' };
(cache.getVolume as jest.Mock)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume2)
.mockReturnValueOnce(mockVolume)
.mockReturnValueOnce(mockVolume2);
const mockMatchDetailsWithBothVolumes = new Map([
[
'viewport-1',
{
displaySetsInfo: [
{ displaySetInstanceUID: 'test-volume-id' },
{ displaySetInstanceUID: 'test-volume-id-2' },
],
},
],
]);
(compact as jest.Mock).mockReturnValue([mockImageLoadRequest]);
(flatten as jest.Mock).mockReturnValue([mockImageLoadRequest]);
interleaveCenterLoader({
...defaultParameters,
data: {
...defaultParameters.data,
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
},
viewportMatchDetails: mockMatchDetailsWithBothVolumes,
});
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
});
});
Loaded 100 of 435 files, more files were not shown because too many files have changed in this diff. Show more