Compare commits
458
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9125cec333 | ||
|
|
87d623a76e | ||
|
|
e849199eb0 | ||
|
|
0495020389 | ||
|
|
98ab95a743 | ||
|
|
ec685ef5b9 | ||
|
|
70eb2aa6d7 | ||
|
|
a1597d7bd6 | ||
|
|
8f0035aeef | ||
|
|
c8923c5499 | ||
|
|
c0c4f2398c | ||
|
|
60a8d5154a | ||
|
|
efc554bd49 | ||
|
|
fc5f73bdfd | ||
|
|
579a592812 | ||
|
|
ea961d6b66 | ||
|
|
69a620ac68 | ||
|
|
8eaebd015d | ||
|
|
0326496c47 | ||
|
|
327f2cb1cf | ||
|
|
478def8447 | ||
|
|
0a9cbb7589 | ||
|
|
c5752e756a | ||
|
|
3b20d2b816 | ||
|
|
0d4cb69ec3 | ||
|
|
4f18a21868 | ||
|
|
c4299de984 | ||
|
|
a11cd6d6cb | ||
|
|
a72192da28 | ||
|
|
0dca891ff3 | ||
|
|
b4f08adfb6 | ||
|
|
4d87e4dbda | ||
|
|
6a02f0b41c | ||
|
|
43efed207e | ||
|
|
4f2583483c | ||
|
|
e3f7838ce0 | ||
|
|
08d329add4 | ||
|
|
579c0c9d01 | ||
|
|
ea7a3e07cb | ||
|
|
e91811ff23 | ||
|
|
dd8e69a8cf | ||
|
|
b1b66076b9 | ||
|
|
80b1f54af1 | ||
|
|
75fde87b59 | ||
|
|
64f2f50f63 | ||
|
|
7f17d5d733 | ||
|
|
9ff4ece541 | ||
|
|
e534802abb | ||
|
|
51a87028c1 | ||
|
|
eba85aa5b3 | ||
|
|
331778dfef | ||
|
|
daa5a857e4 | ||
|
|
cb1f3b877b | ||
|
|
f748d2a73a | ||
|
|
365d824b98 | ||
|
|
ee8962e9a7 | ||
|
|
cde457c626 | ||
|
|
5ef102681d | ||
|
|
d55e794a86 | ||
|
|
4f7fa35def | ||
|
|
1f09527ae6 | ||
|
|
118922f365 | ||
|
|
926f549e91 | ||
|
|
0d900bf4ad | ||
|
|
1bfce0a03c | ||
|
|
c08debd65a | ||
|
|
ab5cead2af | ||
|
|
7a96406a11 | ||
|
|
4432b6b420 | ||
|
|
7670941256 | ||
|
|
e1feffa425 | ||
|
|
3b47e6971c | ||
|
|
13ea778e61 | ||
|
|
14498d4e9a | ||
|
|
83f3bf4ec1 | ||
|
|
3b96c9e771 | ||
|
|
da8bdb94a9 | ||
|
|
1f2c7c4c57 | ||
|
|
5c7a8d0c56 | ||
|
|
b2c0faf29d | ||
|
|
4068488bb2 | ||
|
|
acc6a217c9 | ||
|
|
7599ec9421 | ||
|
|
b12e0211d4 | ||
|
|
689008eb82 | ||
|
|
7377db8d28 | ||
|
|
a2be64f899 | ||
|
|
5a2e88455a | ||
|
|
ab6e341731 | ||
|
|
45f3a2d84d | ||
|
|
00f78adb1f | ||
|
|
8e7cc11529 | ||
|
|
c21d4848ef | ||
|
|
1de19a07c1 | ||
|
|
9b10303a2e | ||
|
|
c6d3990341 | ||
|
|
c42cd90c1d | ||
|
|
90364189b8 | ||
|
|
00707db1ad | ||
|
|
688d896f3e | ||
|
|
72bef63ef6 | ||
|
|
50669867c9 | ||
|
|
90d974b90c | ||
|
|
aa0e5a5937 | ||
|
|
dd2648a080 | ||
|
|
0dbf85a3d5 | ||
|
|
d402ded8c3 | ||
|
|
b1514b9a3e | ||
|
|
bbfa3c1a1f | ||
|
|
ba8af0f35b | ||
|
|
5755ce5430 | ||
|
|
a7cba1a3cf | ||
|
|
16b7aa4f65 | ||
|
|
9f0828ae27 | ||
|
|
8903e3c3dd | ||
|
|
6231629b38 | ||
|
|
b9eb54c71f | ||
|
|
2656beef0e | ||
|
|
fc0064fd9d | ||
|
|
9f3fc382c3 | ||
|
|
640200ba7e | ||
|
|
246ebab6eb | ||
|
|
58da0324cd | ||
|
|
c9a2496aab | ||
|
|
4368eb3896 | ||
|
|
fc9b521fa9 | ||
|
|
dcd1e4eb28 | ||
|
|
0968b3abb1 | ||
|
|
f6cdbe46bc | ||
|
|
d8ae7b884b | ||
|
|
38c19fab77 | ||
|
|
4b91ce4df9 | ||
|
|
6fb7efc46d | ||
|
|
aefa6d94df | ||
|
|
998302eb85 | ||
|
|
400e914011 | ||
|
|
31271aeef7 | ||
|
|
993308b32b | ||
|
|
3a0b36d4e4 | ||
|
|
24c511f4bc | ||
|
|
6d11048ca5 | ||
|
|
6fd3c7e293 | ||
|
|
a2084f319b | ||
|
|
2845f07806 | ||
|
|
78b814224e | ||
|
|
b2c629123c | ||
|
|
6102e5d80f | ||
|
|
9b9e5be477 | ||
|
|
32762e6bce | ||
|
|
6faf3a17f9 | ||
|
|
bd742d19b2 | ||
|
|
5531487fd6 | ||
|
|
6c7d01a184 | ||
|
|
369851d87f | ||
|
|
16cfab2aca | ||
|
|
e60d3bc069 | ||
|
|
a9e0cc5518 | ||
|
|
2048b19484 | ||
|
|
257cc477cd | ||
|
|
c9362726eb | ||
|
|
02eb256dc7 | ||
|
|
897de44856 | ||
|
|
cfb06e458b | ||
|
|
f660f8e970 | ||
|
|
bb2f5625fc | ||
|
|
a883f0de66 | ||
|
|
d09f8b5ba2 | ||
|
|
976003d665 | ||
|
|
8714f01f9d | ||
|
|
fdf883ada8 | ||
|
|
bea56d4f66 | ||
|
|
6c38381ac2 | ||
|
|
75ef99d3b6 | ||
|
|
ff1d97f76e | ||
|
|
2155b0641d | ||
|
|
c9cc372581 | ||
|
|
2bcdb52db9 | ||
|
|
16751ee141 | ||
|
|
71a1e561bf | ||
|
|
aef00e91d6 | ||
|
|
d5d821464a | ||
|
|
9d0b729dc5 | ||
|
|
f3bbfff09b | ||
|
|
b6136acff6 | ||
|
|
a433d406e2 | ||
|
|
6f93449149 | ||
|
|
e462fd31f7 | ||
|
|
88575c6c09 | ||
|
|
01e200986c | ||
|
|
0ac7b2faaa | ||
|
|
d8f6991dbe | ||
|
|
6fc4390645 | ||
|
|
29d522f5f9 | ||
|
|
2eab049d79 | ||
|
|
c7a2000873 | ||
|
|
bd6917a2ca | ||
|
|
097ef76655 | ||
|
|
729efb6d76 | ||
|
|
2f2f6b4c8d | ||
|
|
36a015a3ae | ||
|
|
6971287460 | ||
|
|
a97666ea4e | ||
|
|
5a4783cb1a | ||
|
|
4e87317db6 | ||
|
|
d5d004292e | ||
|
|
7ed75bc4e9 | ||
|
|
55dcfa1f69 | ||
|
|
efa6df8bfb | ||
|
|
58bde5982f | ||
|
|
09853abc52 | ||
|
|
727088f3dc | ||
|
|
89a0fae244 | ||
|
|
4a3b37c6bb | ||
|
|
f044587b73 | ||
|
|
7bf9e6214f | ||
|
|
33f4c18f26 | ||
|
|
319b31d9a8 | ||
|
|
14221eb78e | ||
|
|
bfb6fc5be7 | ||
|
|
9f8ad28d71 | ||
|
|
f93cdf326d | ||
|
|
6d54132386 | ||
|
|
649ffab4d9 | ||
|
|
35077f1f02 | ||
|
|
24fe144fa8 | ||
|
|
a29e94de80 | ||
|
|
d747c97be3 | ||
|
|
95c47ed16c | ||
|
|
430330f7e3 | ||
|
|
ddc7559e16 | ||
|
|
abf9197b15 | ||
|
|
ab1a18af5a | ||
|
|
4761bbf0c8 | ||
|
|
8a3367279a | ||
|
|
f85da32f34 | ||
|
|
6e6ecd8709 | ||
|
|
e91827f002 | ||
|
|
93e760937f | ||
|
|
5945887b22 | ||
|
|
b8b65eac77 | ||
|
|
2174026789 | ||
|
|
d7aa386800 | ||
|
|
af2237e7c7 | ||
|
|
4dfb68f9cc | ||
|
|
19b5b1c15c | ||
|
|
c02a4f2e26 | ||
|
|
756c2f88e6 | ||
|
|
e073d195fd | ||
|
|
4ce7831ea7 | ||
|
|
fe4dfb63e4 | ||
|
|
731340d70a | ||
|
|
395894c6a9 | ||
|
|
8c25e7d557 | ||
|
|
18ac08ed86 | ||
|
|
cb30b97610 | ||
|
|
22d5e51751 | ||
|
|
75a71db7f8 | ||
|
|
c6a3ce4804 | ||
|
|
a389daff2f | ||
|
|
18686c2caf | ||
|
|
07eb3c527d | ||
|
|
96860292ea | ||
|
|
7745c092bb | ||
|
|
b0af8ec553 | ||
|
|
2114ffff41 | ||
|
|
70f2c797f4 | ||
|
|
72b63dcd6a | ||
|
|
268569e8c4 | ||
|
|
8960b89911 | ||
|
|
8c5b228d27 | ||
|
|
a547b47e2d | ||
|
|
6ca158e4f6 | ||
|
|
8c6ae839ef | ||
|
|
1ffe71d52a | ||
|
|
628132eb86 | ||
|
|
9a8ee2b3fb | ||
|
|
023fde9b26 | ||
|
|
2f2df1826b | ||
|
|
590a5a81d7 | ||
|
|
c0ff0698f1 | ||
|
|
73d9e99d5d | ||
|
|
1124c409e9 | ||
|
|
a394f4cea7 | ||
|
|
34c7d72142 | ||
|
|
7b24cc5038 | ||
|
|
5da97511e3 | ||
|
|
4fcee5d449 | ||
|
|
24291c3a11 | ||
|
|
c2132beced | ||
|
|
b124c91d8f | ||
|
|
bf3acd076d | ||
|
|
925d18b463 | ||
|
|
5903b0749a | ||
|
|
ebe39cb8e6 | ||
|
|
de628ff814 | ||
|
|
29944c8512 | ||
|
|
3ea60c3044 | ||
|
|
8ecf3424d1 | ||
|
|
fe1a706446 | ||
|
|
856cd17531 | ||
|
|
3d27d8079a | ||
|
|
70bb6c4626 | ||
|
|
d4b09738fb | ||
|
|
533cb26d53 | ||
|
|
dce1e7d423 | ||
|
|
0d81ecf166 | ||
|
|
de06a98312 | ||
|
|
fb91c6da69 | ||
|
|
391ff2243d | ||
|
|
3227ec1ddc | ||
|
|
8d5c676a5e | ||
|
|
bbd41ac3dd | ||
|
|
e096aa8b1a | ||
|
|
6bba2e70f8 | ||
|
|
d5759d6c06 | ||
|
|
a289c10eb2 | ||
|
|
997a1afd7c | ||
|
|
953d9eed9c | ||
|
|
c86a141119 | ||
|
|
8fa339f296 | ||
|
|
2de102c73c | ||
|
|
e341ebe9d6 | ||
|
|
2046a16dad | ||
|
|
374065bc3b | ||
|
|
b2cd0172d6 | ||
|
|
cf8a99fbd3 | ||
|
|
730f434910 | ||
|
|
01fe7fab3d | ||
|
|
4ca8595095 | ||
|
|
0156bc426f | ||
|
|
8c304718ad | ||
|
|
000224ce1a | ||
|
|
f95255e8cd | ||
|
|
351244e8f3 | ||
|
|
e4749f722c | ||
|
|
e4b6478952 | ||
|
|
6a8f4b9f38 | ||
|
|
956fff4e5b | ||
|
|
f1a736d193 | ||
|
|
4784638717 | ||
|
|
49e82f4756 | ||
|
|
c09311d3b7 | ||
|
|
b4ed80d99b | ||
|
|
c87081d924 | ||
|
|
f09dfaf911 | ||
|
|
11a4ba3c99 | ||
|
|
56ccb4f7e0 | ||
|
|
6ebd2cc7cb | ||
|
|
25c01fc66c | ||
|
|
2ea882e299 | ||
|
|
52a0c59294 | ||
|
|
3ec7e51216 | ||
|
|
1b366c92ee | ||
|
|
13d67d90fc | ||
|
|
f0ed3fd7b9 | ||
|
|
65d419dd52 | ||
|
|
4349fc1085 | ||
|
|
8192e348ec | ||
|
|
8c5ab942b9 | ||
|
|
ab19d9c0a2 | ||
|
|
d250d04580 | ||
|
|
28bc8914a8 | ||
|
|
3952d465dc | ||
|
|
1a013909a1 | ||
|
|
8f42baf36e | ||
|
|
2633ef7323 | ||
|
|
da7c1a5d8c | ||
|
|
058da93863 | ||
|
|
bc819409b3 | ||
|
|
170bb96983 | ||
|
|
c4e6556259 | ||
|
|
ab61cc9b1f | ||
|
|
3754c224b4 | ||
|
|
d3cdfd8506 | ||
|
|
aa572daa2c | ||
|
|
f99a0bfb31 | ||
|
|
a6c6fff43a | ||
|
|
e687384933 | ||
|
|
e80fc6f477 | ||
|
|
41a8d105f1 | ||
|
|
60b2f69334 | ||
|
|
df49a653be | ||
|
|
77693ddef4 | ||
|
|
6a757f69d8 | ||
|
|
2b83393f91 | ||
|
|
ac0e1c0a2c | ||
|
|
b0b65c582d | ||
|
|
4f99244d86 | ||
|
|
be9bb06565 | ||
|
|
c7e53bdbf8 | ||
|
|
7c3d67c2a8 | ||
|
|
e9c30a108e | ||
|
|
603d5cc53a | ||
|
|
5cc693d26c | ||
|
|
7fac49b449 | ||
|
|
7014e6ca4a | ||
|
|
87b68bba4a | ||
|
|
d57e8bc157 | ||
|
|
dc0b183dd4 | ||
|
|
06920ab927 | ||
|
|
dc37802ec1 | ||
|
|
894a227978 | ||
|
|
1c9fee1bec | ||
|
|
5645ac1b27 | ||
|
|
6e5892d202 | ||
|
|
9d8997ad84 | ||
|
|
cea27d438d | ||
|
|
78fd8cce86 | ||
|
|
f7c9a5cb64 | ||
|
|
56b1eae635 | ||
|
|
807afb8ce5 | ||
|
|
5f494598bb | ||
|
|
fa38e6a07a | ||
|
|
d8f66856cc | ||
|
|
9c39689451 | ||
|
|
9f3235ff09 | ||
|
|
e8e209291c | ||
|
|
bc71861619 | ||
|
|
b7f90e3951 | ||
|
|
07301fc020 | ||
|
|
1203477dd4 | ||
|
|
158a181670 | ||
|
|
aac3546bed | ||
|
|
1256a4d565 | ||
|
|
617043fe0d | ||
|
|
6f610b09e1 | ||
|
|
a2e320f9f8 | ||
|
|
566b25a544 | ||
|
|
79d5c36bda | ||
|
|
f47a94317d | ||
|
|
45b68e841d | ||
|
|
d70aae3233 | ||
|
|
345b137bec | ||
|
|
9bcd1ae6f5 | ||
|
|
6b79dff5c1 | ||
|
|
88859f75d5 | ||
|
|
feed2304c1 | ||
|
|
08815909c7 | ||
|
|
ed2234a67c | ||
|
|
f7fe91c5f6 | ||
|
|
21ec6860f2 | ||
|
|
ac474b7f8f | ||
|
|
e485d68fd4 | ||
|
|
5089229ff9 | ||
|
|
df09176d63 | ||
|
|
bb603bd1f8 | ||
|
|
82a4a7d377 | ||
|
|
c27fa9c802 | ||
|
|
4cdfdae814 | ||
|
|
596340803a | ||
|
|
ff4e17f5c2 | ||
|
|
21e8a2bd0b | ||
|
|
2a442e30eb | ||
|
|
8e8a66bd29 | ||
|
|
b3a0faf5f5 | ||
|
|
a2a0090eeb | ||
|
|
ea552027c5 | ||
|
|
0eac386a31 |
No files matched your search
+16
-4
@@ -13,7 +13,7 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@3.2.1
|
||||
cypress: cypress-io/cypress@3.3.1
|
||||
|
||||
executors:
|
||||
cypress-custom:
|
||||
@@ -42,6 +42,7 @@ jobs:
|
||||
###
|
||||
UNIT_TESTS:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
# Update yarn
|
||||
- run: yarn -v
|
||||
@@ -98,6 +99,7 @@ jobs:
|
||||
###
|
||||
BUILD:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout
|
||||
@@ -146,6 +148,7 @@ jobs:
|
||||
# just to make sure later on we can publish them
|
||||
BUILD_PACKAGES_QUICK:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- run: yarn -v
|
||||
# Checkout code and ALL Git Tags
|
||||
@@ -195,6 +198,7 @@ jobs:
|
||||
###
|
||||
NPM_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- run: yarn -v
|
||||
# Checkout code and ALL Git Tags
|
||||
@@ -264,6 +268,7 @@ jobs:
|
||||
|
||||
DOCKER_RELEASE_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
@@ -293,6 +298,7 @@ jobs:
|
||||
|
||||
DOCKER_BETA_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
@@ -379,7 +385,7 @@ jobs:
|
||||
- yarn-berry
|
||||
type: enum
|
||||
parallelism:
|
||||
default: 1
|
||||
default: 4
|
||||
description: |
|
||||
Number of Circle machines to use for load balancing, min 1
|
||||
(requires `parallel` and `record` flags in your `cypress-command`)
|
||||
@@ -397,6 +403,7 @@ jobs:
|
||||
default: ''
|
||||
description: Directory containing package.json
|
||||
type: string
|
||||
resource_class: large
|
||||
steps:
|
||||
- cypress/install:
|
||||
cypress-cache-key: << parameters.cypress-cache-key >>
|
||||
@@ -495,13 +502,18 @@ workflows:
|
||||
- BUILD:
|
||||
filters:
|
||||
branches:
|
||||
only: release
|
||||
- NPM_PUBLISH:
|
||||
only: /^release\/.*/
|
||||
- HOLD_FOR_APPROVAL:
|
||||
type: approval
|
||||
requires:
|
||||
- BUILD
|
||||
- NPM_PUBLISH:
|
||||
requires:
|
||||
- HOLD_FOR_APPROVAL
|
||||
- DOCKER_RELEASE_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
|
||||
###
|
||||
# Unit and E2E tests have already run for PR_CHECKS
|
||||
# Re-running should not gain us any confidence here
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# ABOUT:
|
||||
# https://docs.codecov.io/docs/codecov-yaml
|
||||
#
|
||||
#
|
||||
# COMMIT STATUS:
|
||||
# https://docs.codecov.io/docs/commit-status
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
||||
core:
|
||||
flags: core
|
||||
threshold: 0.5%
|
||||
viewer:
|
||||
flags: viewer
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
flags:
|
||||
core:
|
||||
paths:
|
||||
- platform/core
|
||||
viewer:
|
||||
paths:
|
||||
- platform/app
|
||||
@@ -1 +0,0 @@
|
||||
PERCY_TOKEN=<your token here>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
playwright-tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5]
|
||||
shardTotal: [5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: export NODE_OPTIONS="--max_old_space_size=8192" && npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [playwright-tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
+13
-2
@@ -10,6 +10,8 @@ junit.xml
|
||||
coverage/
|
||||
.docz/
|
||||
.yarn/
|
||||
.nx/
|
||||
addOns/yarn.lock
|
||||
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
@@ -24,6 +26,7 @@ yarn-error.log
|
||||
.DS_Store
|
||||
.env
|
||||
*.code-workspace
|
||||
.directory
|
||||
|
||||
# Common Example Data Directories
|
||||
sampledata/
|
||||
@@ -32,7 +35,7 @@ docker/dcm4che/dcm4che-arc
|
||||
|
||||
# Cypress test results
|
||||
videos/
|
||||
screenshots/
|
||||
|
||||
|
||||
# Locize settings
|
||||
.locize
|
||||
@@ -40,8 +43,16 @@ screenshots/
|
||||
# autogenerated files
|
||||
platform/app/src/pluginImports.js
|
||||
/Viewers.iml
|
||||
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
|
||||
platform/app/.recipes/Nginx-Dcm4Chee/logs/*
|
||||
platform/app/.recipes/OpenResty-Orthanc/logs/*
|
||||
.vercel
|
||||
|
||||
.vs
|
||||
|
||||
# PlayWright
|
||||
|
||||
node_modules/
|
||||
tests/test-results/
|
||||
tests/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
@@ -4,26 +4,34 @@
|
||||
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
|
||||
* in the DICOM JSON Data source on docs.ohif.org
|
||||
*
|
||||
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
|
||||
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
|
||||
*
|
||||
* params:
|
||||
* - studyFolder: path to the study folder
|
||||
* - studyFolder: path to the study folder which contains the DICOM files
|
||||
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
|
||||
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
|
||||
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
|
||||
* json file for the first instance of the first series of the first study will be
|
||||
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
|
||||
*
|
||||
* as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when
|
||||
* the .dcm file is hosted statically and can be accessed via a URL (like our example above)
|
||||
* However, you can specify a new scheme bellow.
|
||||
*
|
||||
* - outputJSONPath: path to the output JSON file
|
||||
* - scheme: default dicomweb if not provided
|
||||
*/
|
||||
const dcmjs = require('dcmjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const [studyDirectory, urlPrefix, outputPath] = args;
|
||||
const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args;
|
||||
|
||||
if (args.length !== 3) {
|
||||
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
|
||||
if (args.length < 3 || args.length > 4) {
|
||||
console.error(
|
||||
'Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -31,7 +39,7 @@ const model = {
|
||||
studies: [],
|
||||
};
|
||||
|
||||
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
|
||||
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) {
|
||||
try {
|
||||
const files = await recursiveReadDir(studyDirectory);
|
||||
console.debug('Processing...');
|
||||
@@ -42,7 +50,7 @@ async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
|
||||
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
|
||||
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
|
||||
|
||||
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
|
||||
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme);
|
||||
processInstance(instance);
|
||||
}
|
||||
}
|
||||
@@ -77,10 +85,10 @@ async function recursiveReadDir(dir) {
|
||||
return results;
|
||||
}
|
||||
|
||||
function createImageId(fileLocation, urlPrefix, studyDirectory) {
|
||||
function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) {
|
||||
const relativePath = path.relative(studyDirectory, fileLocation);
|
||||
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
|
||||
return `dicomweb:${urlPrefix}${normalizedPath}`;
|
||||
return `${scheme}:${urlPrefix}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function processInstance(instance) {
|
||||
@@ -262,4 +270,4 @@ function createInstanceMetaDataMultiFrame(instance) {
|
||||
return instances;
|
||||
}
|
||||
|
||||
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);
|
||||
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme);
|
||||
Vendored
+1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mikestead.dotenv",
|
||||
"bungcip.better-toml",
|
||||
|
||||
Vendored
+90
-12
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.rulers": [80, 120],
|
||||
// ===
|
||||
// Spacing
|
||||
@@ -14,23 +15,100 @@
|
||||
// Event Triggers
|
||||
// ===
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.autoFixOnSave": true,
|
||||
"eslint.run": "onSave",
|
||||
"eslint.validate": [
|
||||
{
|
||||
"language": "javascript",
|
||||
"autoFix": true
|
||||
},
|
||||
{
|
||||
"language": "javascriptreact",
|
||||
"autoFix": true
|
||||
}
|
||||
],
|
||||
"jest.autoRun": "off",
|
||||
"prettier.disableLanguages": ["html"],
|
||||
"prettier.endOfLine": "lf",
|
||||
"workbench.colorCustomizations": {},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"cSpell.userWords": [
|
||||
"aabb",
|
||||
"architectured",
|
||||
"attrname",
|
||||
"Barksy",
|
||||
"browserslist",
|
||||
"bulkdata",
|
||||
"Cacheable",
|
||||
"cfun",
|
||||
"clonedeep",
|
||||
"Colormap",
|
||||
"Colormaps",
|
||||
"Comlink",
|
||||
"cornerstonejs",
|
||||
"Crosshairs",
|
||||
"datasource",
|
||||
"dcmjs",
|
||||
"decache",
|
||||
"decached",
|
||||
"decaching",
|
||||
"deepmerge",
|
||||
"Dicom",
|
||||
"dicomweb",
|
||||
"DISPLAYSETS",
|
||||
"glwindow",
|
||||
"grababble",
|
||||
"grabbable",
|
||||
"Hounsfield",
|
||||
"Interactable",
|
||||
"Interactor",
|
||||
"istyle",
|
||||
"kitware",
|
||||
"labelmap",
|
||||
"labelmaps",
|
||||
"livewire",
|
||||
"Mergeable",
|
||||
"multiframe",
|
||||
"nifti",
|
||||
"ofun",
|
||||
"OHIF",
|
||||
"polylines",
|
||||
"POLYSEG",
|
||||
"prapogation",
|
||||
"precisionmetrics",
|
||||
"prefetch",
|
||||
"Prescaled",
|
||||
"pydicom",
|
||||
"Radiopharmaceutical",
|
||||
"rasterizing",
|
||||
"reconstructable",
|
||||
"Rehydratable",
|
||||
"renderable",
|
||||
"resampler",
|
||||
"resemblejs",
|
||||
"reslice",
|
||||
"resliced",
|
||||
"Reslices",
|
||||
"roadmap",
|
||||
"ROADMAPS",
|
||||
"Segmentations",
|
||||
"semibold",
|
||||
"sitk",
|
||||
"SUBRESOLUTION",
|
||||
"suvbsa",
|
||||
"suvbw",
|
||||
"suvlbm",
|
||||
"textbox",
|
||||
"thresholded",
|
||||
"thresholding",
|
||||
"timepoint",
|
||||
"timepoints",
|
||||
"TMTV",
|
||||
"TOOLGROUP",
|
||||
"tqdm",
|
||||
"transferables",
|
||||
"typedoc",
|
||||
"unsubscriptions",
|
||||
"uuidv",
|
||||
"viewplane",
|
||||
"viewports",
|
||||
"Voxel",
|
||||
"Voxels",
|
||||
"Vtkjs",
|
||||
"wado",
|
||||
"wadors",
|
||||
"wadouri",
|
||||
"workerpool"
|
||||
]
|
||||
}
|
||||
@@ -7,18 +7,21 @@ const fs = require('fs');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// ~~ PLUGINS
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
|
||||
// ~~ PackageJSON
|
||||
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
|
||||
// .rules;
|
||||
// ~~ RULES
|
||||
const loadShadersRule = require('./rules/loadShaders.js');
|
||||
// const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
|
||||
const cssToJavaScript = require('./rules/cssToJavaScript.js');
|
||||
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
|
||||
// Only uncomment for old v2 stylus
|
||||
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
|
||||
|
||||
// ~~ ENV VARS
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
@@ -98,6 +101,46 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
module: {
|
||||
noParse: [/(codec)/, /(dicomicc)/],
|
||||
rules: [
|
||||
...(isProdBuild ? [] : [{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: ['react-refresh/babel'],
|
||||
},
|
||||
}]),
|
||||
{
|
||||
test: /\.svg?$/,
|
||||
oneOf: [
|
||||
{
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
svgoConfig: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
prettier: false,
|
||||
svgo: true,
|
||||
titleProp: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
issuer: {
|
||||
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
@@ -120,6 +163,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
test: /\.wasm/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'assets/images/[name].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
], //.concat(vtkRules),
|
||||
},
|
||||
resolve: {
|
||||
@@ -131,8 +185,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
|
||||
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
|
||||
'@state': path.resolve(__dirname, '../platform/app/src/state'),
|
||||
'dicom-microscopy-viewer':
|
||||
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
|
||||
'@cornerstonejs/dicom-image-loader':
|
||||
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
|
||||
},
|
||||
@@ -162,6 +214,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// new BundleAnalyzerPlugin(),
|
||||
],
|
||||
|
||||
+1448
File diff suppressed because it is too large.
Load diff
+141
@@ -10,6 +10,7 @@ Please find below the list of datasets used on the demo page, along with their r
|
||||
|
||||
|
||||
## Platforms
|
||||
|
||||
### NIH NCI IDC
|
||||
|
||||
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
|
||||
@@ -147,3 +148,143 @@ Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
|
||||
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
|
||||
|
||||
https://data.kitware.com/
|
||||
|
||||
|
||||
### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772
|
||||
### 2.25.269859997690759739055099378767846712697
|
||||
### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868
|
||||
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696
|
||||
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
|
||||
### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1
|
||||
### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1
|
||||
### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1
|
||||
|
||||
https://github.com/ImagingInformatics/hackathon-images
|
||||
|
||||
### 2.16.124.113543.6004.101.103.20021117.162333.1
|
||||
### 2.16.124.113543.6004.101.103.20021117.190619.1
|
||||
### 2.16.124.113543.6004.101.103.20021117.123455.1
|
||||
### 2.16.124.113543.6004.101.103.20021117.061159.1
|
||||
|
||||
https://www.aapm.org/
|
||||
|
||||
|
||||
### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886
|
||||
|
||||
|
||||
### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612
|
||||
|
||||
Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor.
|
||||
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.9328.50.1.54652
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/rider-pilot/
|
||||
|
||||
Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986
|
||||
|
||||
Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer – Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472
|
||||
|
||||
Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807
|
||||
### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681
|
||||
|
||||
Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556
|
||||
|
||||
|
||||
Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank – Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/cmb-aml/
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/
|
||||
|
||||
Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/cmb-pca/
|
||||
|
||||
Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank – Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12
|
||||
|
||||
### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331
|
||||
|
||||
Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/cptac-ucec/
|
||||
|
||||
|
||||
National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/remind/
|
||||
|
||||
Juvekar, P., Dorent, R., Kögl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070
|
||||
|
||||
### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010
|
||||
|
||||
Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/phantom-fda/
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/4d-lung/
|
||||
|
||||
|
||||
Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/
|
||||
|
||||
Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/prostate-mri/
|
||||
|
||||
Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/upenn-gbm/
|
||||
|
||||
Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., … Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/breast-diagnosis/
|
||||
|
||||
Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
|
||||
|
||||
|
||||
### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065
|
||||
|
||||
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
|
||||
|
||||
https://www.cancerimagingarchive.net/collection/ea1141/
|
||||
@@ -41,17 +41,19 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
|
||||
|
||||
| | | |
|
||||
| :-: | :--- | :--- |
|
||||
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
|
||||
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-measurements.webp?raw=true" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-segmentation.webp?raw=true" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ptct.webp?raw=true" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
|
||||
|
||||
## About
|
||||
|
||||
The OHIF Medical Imaging Viewer is for viewing medical images. It can retrieve
|
||||
The OHIF Viewer can retrieve
|
||||
and load images from most sources and formats; render sets in 2D, 3D, and
|
||||
reconstructed representations; allows for the manipulation, annotation, and
|
||||
serialization of observations; supports internationalization, OpenID Connect,
|
||||
@@ -116,19 +118,16 @@ This is typically where the latest development happens. Code that is in the mast
|
||||
|
||||
Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1`
|
||||
|
||||
### `release` branch - The latest stable release
|
||||
### `release/*` branches - The latest stable releases
|
||||
Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production.
|
||||
|
||||
This branch represents the latest stable version of the project that is considered ready for production. The code in this branch should be fully tested and vetted for release. Once the code in the master branch reaches a state where it's stable and ready to be released to users,
|
||||
we do a comprehensive code review and QA testing. Once the code is approved,
|
||||
we merge it into the release branch and tag a new release.
|
||||
For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch.
|
||||
|
||||
Each package is tagged with version numbers, and published to npm such as `@ohif/ui@3.5.0`
|
||||
|
||||
Note: `master` is always ahead of `release` branch. We publish both docker builds for beta and stable releases.
|
||||
Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases.
|
||||
|
||||
Here is a schematic representation of our development workflow:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -137,7 +136,7 @@ Here is a schematic representation of our development workflow:
|
||||
### Requirements
|
||||
|
||||
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
||||
- [Node 16+](https://nodejs.org/en/)
|
||||
- [Node 18+](https://nodejs.org/en/)
|
||||
- Yarn Workspaces should be enabled on your machine:
|
||||
- `yarn config set workspaces-experimental true`
|
||||
|
||||
@@ -334,4 +333,4 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license)
|
||||
@@ -0,0 +1,3 @@
|
||||
# External Dependencies
|
||||
|
||||
This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component.
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd))
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"name": "@externals/devDependencies",
|
||||
"description": "External dev dependencies - put dev build dependencies here",
|
||||
"version": "3.9.0-beta.97",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"yarn": ">=1.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"clsx": "^2.1.1",
|
||||
"core-js": "^3.2.1",
|
||||
"moment": "^2.9.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"babel-eslint": "9.x",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-flowtype": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.11",
|
||||
"eslint-webpack-plugin": "^2.5.3",
|
||||
"execa": "^8.0.1",
|
||||
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-canvas-mock": "^2.1.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-junit": "^6.4.0",
|
||||
"lerna": "^7.2.0",
|
||||
"lint-staged": "^9.0.2",
|
||||
"mini-css-extract-plugin": "^2.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
||||
"postcss": "^8.3.5",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-loader": "^6.1.1",
|
||||
"postcss-preset-env": "^7.4.3",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"react-refresh": "^0.14.2",
|
||||
"semver": "^7.5.1",
|
||||
"serve": "^14.2.0",
|
||||
"shader-loader": "^1.3.1",
|
||||
"shx": "^0.3.3",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"start-server-and-test": "^1.10.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"stylus": "^0.59.0",
|
||||
"stylus-loader": "^7.1.3",
|
||||
"terser-webpack-plugin": "^5.1.4",
|
||||
"typescript": "4.6.4",
|
||||
"unused-webpack-plugin": "2.4.0",
|
||||
"webpack": "5.89.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "4.7.3",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"workbox-webpack-plugin": "^6.1.5",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "Included as direct dependency"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd))
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@externals/dicom-microscopy-viewer",
|
||||
"description": "External reference to dicom-microscopy-viewer",
|
||||
"version": "3.9.0-beta.97",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dicom-microscopy-viewer": "^0.46.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "ohif-monorepo-root",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"../platform/i18n",
|
||||
"../platform/core",
|
||||
"../platform/ui",
|
||||
"../platform/ui-next",
|
||||
"../platform/app",
|
||||
"../extensions/*",
|
||||
"../modes/*",
|
||||
"../addOns/externals/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"**/html-minifier-terser"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "cd .. && node preinstall.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.16.7",
|
||||
"@babel/plugin-transform-regenerator": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/plugin-transform-typescript": "^7.13.0",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.13.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@babel/runtime": "^7.20.13",
|
||||
"commander": "8.3.0",
|
||||
"dcmjs": ">=0.33.0",
|
||||
"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": "4.6.4",
|
||||
"sharp": "^0.32.6"
|
||||
}
|
||||
}
|
||||
+2
-18
@@ -5,23 +5,6 @@ module.exports = {
|
||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
||||
plugins: [
|
||||
[
|
||||
'inline-react-svg',
|
||||
{
|
||||
svgo: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
'@babel/plugin-transform-typescript',
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
@@ -49,6 +32,7 @@ module.exports = {
|
||||
'@babel/transform-destructuring',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
@@ -67,7 +51,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
5bba98ed848bdf46b5ba4fc4708527cced3308b5
|
||||
e849199eb0a9ecba4f9845aa1e07df775d5ded9b
|
||||
@@ -0,0 +1,12 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.tsx`,
|
||||
};
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '../');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.tsx`,
|
||||
};
|
||||
|
||||
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
|
||||
return merge(commonConfig, {
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'ohif-extension-cornerstone-dicom-pmap',
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b))
|
||||
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
# dicom-pmap
|
||||
## Description
|
||||
|
||||
DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric
|
||||
Map image and display it on OHIF.
|
||||
|
||||
## Author
|
||||
|
||||
OHIF
|
||||
|
||||
## License
|
||||
MIT
|
||||
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
[
|
||||
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: 'commonjs',
|
||||
debug: false,
|
||||
},
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
presets: [
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
development: {
|
||||
presets: [
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "DICOM Parametric Map read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
|
||||
"module": "src/index.tsx",
|
||||
"files": [
|
||||
"dist/**",
|
||||
"public/**",
|
||||
"README.md"
|
||||
],
|
||||
"repository": "OHIF/Viewers",
|
||||
"keywords": [
|
||||
"ohif-extension"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:dicom-pmap": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package-1": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.97",
|
||||
"@ohif/extension-default": "3.9.0-beta.97",
|
||||
"@ohif/i18n": "3.9.0-beta.97",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
||||
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomLoaderService } from '@ohif/extension-cornerstone';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPClassUID,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
wadoUriRoot,
|
||||
} = instance;
|
||||
|
||||
const displaySet = {
|
||||
// Parametric map use to have the same modality as its referenced volume but
|
||||
// "PMAP" is used in the viewer even though this is not a valid DICOM modality
|
||||
Modality: 'PMAP',
|
||||
isReconstructable: true, // by default for now
|
||||
displaySetInstanceUID: `pmap.${utils.guid()}`,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
referencedImages: null,
|
||||
referencedSeriesInstanceUID: null,
|
||||
referencedDisplaySetInstanceUID: null,
|
||||
referencedVolumeURI: null,
|
||||
referencedVolumeId: null,
|
||||
isDerivedDisplaySet: true,
|
||||
loadStatus: {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
},
|
||||
sopClassUids,
|
||||
instance,
|
||||
instances: [instance],
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
|
||||
if (!referencedSeriesSequence) {
|
||||
console.error('ReferencedSeriesSequence is missing for the parametric map');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
|
||||
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
// Does not get the referenced displaySet during parametric displaySet creation
|
||||
// because it is still not available (getDisplaySetByUID returns `undefined`).
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
if (displaySet.referencedDisplaySetInstanceUID) {
|
||||
return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
|
||||
}
|
||||
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced displaySet is missing for the parametric map');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
// Does not get the referenced volumeId during parametric displaySet creation because the
|
||||
// referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
|
||||
displaySet.getReferencedVolumeId = () => {
|
||||
if (displaySet.referencedVolumeId) {
|
||||
return displaySet.referencedVolumeId;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySet.getReferenceDisplaySet();
|
||||
const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
|
||||
|
||||
displaySet.referencedVolumeURI = referencedVolumeURI;
|
||||
displaySet.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
return referencedVolumeId;
|
||||
};
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
async function _load(
|
||||
displaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers
|
||||
) {
|
||||
const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
|
||||
const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
|
||||
|
||||
if (volumeLoadObject) {
|
||||
return volumeLoadObject.promise;
|
||||
}
|
||||
|
||||
displaySet.loadStatus.loading = true;
|
||||
|
||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||
// and also return the same promise to any other callers.
|
||||
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
const promise = _loadParametricMap({
|
||||
extensionManager,
|
||||
displaySet,
|
||||
headers,
|
||||
});
|
||||
|
||||
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
displaySet.loadStatus.loading = false;
|
||||
displaySet.loadStatus.loaded = true;
|
||||
})
|
||||
.catch(err => {
|
||||
displaySet.loadStatus.loading = false;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
||||
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
||||
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
||||
|
||||
// Parametric map can be loaded only if its referenced volume exists otherwise it will fail
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
|
||||
);
|
||||
}
|
||||
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
metaData
|
||||
);
|
||||
const { pixelData } = results;
|
||||
const TypedArrayConstructor = pixelData.constructor;
|
||||
const paramMapId = displaySet.displaySetInstanceUID;
|
||||
|
||||
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
|
||||
volumeId: paramMapId,
|
||||
targetBuffer: {
|
||||
type: TypedArrayConstructor.name,
|
||||
},
|
||||
});
|
||||
|
||||
derivedVolume.getScalarData().set(pixelData);
|
||||
|
||||
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
|
||||
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
||||
|
||||
derivedVolume.metadata.voiLut = [windowLevel];
|
||||
derivedVolume.loadStatus = { loaded: true };
|
||||
|
||||
return derivedVolume;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dicom-pmap',
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getSopClassHandlerModule;
|
||||
@@ -0,0 +1,7 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dicom-pmap';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
|
||||
export { id, SOPClassHandlerId, SOPClassHandlerName };
|
||||
@@ -0,0 +1,39 @@
|
||||
import { id } from './id';
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstonePMAPViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const extension = {
|
||||
id,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstonePMAPViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstonePMAPViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
commandsManager={commandsManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }];
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
export default extension;
|
||||
@@ -0,0 +1,6 @@
|
||||
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
export default createPMAPToolGroupAndAddTools;
|
||||
@@ -0,0 +1,163 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
|
||||
|
||||
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
|
||||
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
|
||||
// PMAP viewport will always have a single display set
|
||||
if (displaySets.length !== 1) {
|
||||
throw new Error('PMAP viewport must have a single display set');
|
||||
}
|
||||
|
||||
const pmapDisplaySet = displaySets[0];
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet,
|
||||
pmapDisplaySet
|
||||
);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
displaySet: referencedDisplaySet,
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
displaySetOptions.unshift({});
|
||||
const [pmapDisplaySetOptions] = displaySetOptions;
|
||||
|
||||
// Make sure `options` exists
|
||||
pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {};
|
||||
|
||||
Object.assign(pmapDisplaySetOptions.options, {
|
||||
colormap: {
|
||||
name: 'rainbow',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0.5 },
|
||||
{ value: 1, opacity: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||
></Component>
|
||||
);
|
||||
}, [
|
||||
extensionManager,
|
||||
displaySetOptions,
|
||||
props,
|
||||
pmapDisplaySet,
|
||||
toolGroupId,
|
||||
viewportOptions.orientation,
|
||||
viewportOptions.viewportId,
|
||||
]);
|
||||
|
||||
// Cleanup the PMAP viewport when the viewport is destroyed
|
||||
useEffect(() => {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||
};
|
||||
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
||||
|
||||
useEffect(() => {
|
||||
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
|
||||
if (toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This creates a custom tool group which has the lifetime of this view only
|
||||
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
return () => toolGroupService.destroyToolGroup(toolGroupId);
|
||||
}, [customizationService, toolGroupId, toolGroupService]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{getCornerstoneViewport()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstonePMAPViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) {
|
||||
const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance;
|
||||
|
||||
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
|
||||
? SharedFunctionalGroupsSequence[0]
|
||||
: SharedFunctionalGroupsSequence;
|
||||
|
||||
const { PixelMeasuresSequence } = SharedFunctionalGroup;
|
||||
|
||||
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
|
||||
? PixelMeasuresSequence[0]
|
||||
: PixelMeasuresSequence;
|
||||
|
||||
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
PatientID: image0.PatientID,
|
||||
PatientName: image0.PatientName,
|
||||
PatientSex: image0.PatientSex,
|
||||
PatientAge: image0.PatientAge,
|
||||
SliceThickness: image0.SliceThickness || SliceThickness,
|
||||
StudyDate: image0.StudyDate,
|
||||
SeriesDescription: image0.SeriesDescription,
|
||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||
SeriesNumber: image0.SeriesNumber,
|
||||
ManufacturerModelName: image0.ManufacturerModelName,
|
||||
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
|
||||
};
|
||||
|
||||
return referencedDisplaySetMetadata;
|
||||
}
|
||||
|
||||
export default OHIFCornerstonePMAPViewport;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
@@ -37,7 +37,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.8.0-beta.53",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -24,6 +24,8 @@
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:dicom-seg": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -31,16 +33,16 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.53",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.53",
|
||||
"@ohif/extension-default": "3.8.0-beta.53",
|
||||
"@ohif/i18n": "3.8.0-beta.53",
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.97",
|
||||
"@ohif/extension-default": "3.9.0-beta.97",
|
||||
"@ohif/i18n": "3.9.0-beta.97",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^10.11.0",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
"react-router": "^6.23.1",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
|
||||
+13
-6
@@ -5,9 +5,13 @@ import loadRTStruct from './loadRTStruct';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -69,8 +73,8 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
@@ -90,7 +94,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@@ -168,7 +172,10 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(rtDisplaySet, segmentationService) {
|
||||
function _segmentationExistsInCache(
|
||||
rtDisplaySet,
|
||||
segmentationService: AppTypes.SegmentationService
|
||||
) {
|
||||
// Todo: fix this
|
||||
return false;
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
@@ -13,39 +13,46 @@ function promptHydrateRT({
|
||||
toolGroupId = 'default',
|
||||
preHydrateCallbacks,
|
||||
hydrateRTDisplaySet,
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
|
||||
const extensionManager = servicesManager._extensionManager;
|
||||
const appConfig = extensionManager._appConfig;
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
|
||||
const promptResult = appConfig?.disableConfirmationPrompts
|
||||
? RESPONSE.HYDRATE_SEG
|
||||
: await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
window.setTimeout(async () => {
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
resolve(isHydrated);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
@@ -57,6 +64,7 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
id: 'promptHydrateRT',
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
@@ -66,6 +74,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
function OHIFCornerstoneRTViewport(props) {
|
||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
@@ -27,6 +24,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
@@ -113,12 +111,15 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
viewportType: 'stack',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
@@ -126,7 +127,6 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -282,20 +282,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onStatusClick = async () => {
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a RT and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||
@@ -309,41 +296,47 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onSegmentChange}
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `RT Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{rtIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
@@ -365,11 +358,6 @@ OHIFCornerstoneRTViewport.propTypes = {
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
OHIFCornerstoneRTViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
|
||||
|
||||
@@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
@@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">RTSTRUCT</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">RTSTRUCT</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
@@ -37,7 +37,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.8.0-beta.53",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -24,6 +24,8 @@
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:dicom-seg": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -31,22 +33,22 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.53",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.53",
|
||||
"@ohif/extension-default": "3.8.0-beta.53",
|
||||
"@ohif/i18n": "3.8.0-beta.53",
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.97",
|
||||
"@ohif/extension-default": "3.9.0-beta.97",
|
||||
"@ohif/i18n": "3.9.0-beta.97",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.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"
|
||||
"react-router": "^6.23.1",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.44.1",
|
||||
"@cornerstonejs/tools": "^1.44.1",
|
||||
"@kitware/vtk.js": "29.3.0",
|
||||
"@cornerstonejs/adapters": "^1.81.6",
|
||||
"@cornerstonejs/core": "^1.81.6",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import { Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
segmentation as cornerstoneToolsSegmentation,
|
||||
Enums as cornerstoneToolsEnums,
|
||||
utilities,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
} from './utils/hydrationUtils';
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
@@ -45,7 +47,9 @@ const commandsModule = ({
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
toolGroupService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
@@ -91,6 +95,7 @@ const commandsModule = ({
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const currentSegmentations = segmentationService.getSegmentations();
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
@@ -203,11 +208,16 @@ const commandsModule = ({
|
||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const displaySet = displaySets[0];
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const initialSliceIndex = viewport.getSliceIndex();
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const segDisplaySet = displaySet;
|
||||
const suppressEvents = false;
|
||||
@@ -218,9 +228,11 @@ const commandsModule = ({
|
||||
|
||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
||||
return segmentationId;
|
||||
},
|
||||
initialSliceIndex,
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -251,9 +263,12 @@ const commandsModule = ({
|
||||
labelmapObj.metadata = [];
|
||||
|
||||
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
||||
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
|
||||
segmentationInOHIF.segments.forEach(segment => {
|
||||
// segmentation service already has a color for each segment
|
||||
const segment = segmentationInOHIF?.segments[segmentIndex];
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
const segmentIndex = segment.segmentIndex;
|
||||
const { label, color } = segment;
|
||||
|
||||
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
||||
@@ -263,8 +278,8 @@ const commandsModule = ({
|
||||
const segmentMetadata = {
|
||||
SegmentNumber: segmentIndex.toString(),
|
||||
SegmentLabel: label,
|
||||
SegmentAlgorithmType: 'MANUAL',
|
||||
SegmentAlgorithmName: 'OHIF Brush',
|
||||
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
|
||||
SegmentAlgorithmName: segment?.algorithmName || 'OHIF Brush',
|
||||
RecommendedDisplayCIELabValue,
|
||||
SegmentedPropertyCategoryCodeSequence: {
|
||||
CodeValue: 'T-D0050',
|
||||
@@ -397,6 +412,36 @@ const commandsModule = ({
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
setBrushSize: ({ value, toolNames }) => {
|
||||
const brushSize = Number(value);
|
||||
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
if (toolNames?.length === 0) {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||||
} else {
|
||||
toolNames?.forEach(toolName => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setThresholdRange: ({
|
||||
value,
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
}) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolNames?.forEach(toolName => {
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -424,11 +469,18 @@ const commandsModule = ({
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
setBrushSize: {
|
||||
commandFn: actions.setBrushSize,
|
||||
},
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'SEGMENTATION',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox as NewToolbox } from '@ohif/ui-next';
|
||||
import { Toolbox as OldToolbox } from '@ohif/ui';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import SegmentationToolbox from './panels/SegmentationToolbox';
|
||||
|
||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
|
||||
const getPanelModule = ({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
title,
|
||||
}: withAppTypes) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
const wrappedPanelSegmentation = ({ configuration, renderHeader, getCloseIcon, tab }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
@@ -19,23 +24,57 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const wrappedPanelSegmentationWithTools = ({
|
||||
configuration,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SegmentationToolbox
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="segmentationToolbox"
|
||||
title="Segmentation Tools"
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
@@ -43,6 +82,8 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -64,6 +105,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager, co
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationWithTools,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationNoHeader',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationNoHeader,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
+19
-5
@@ -8,9 +8,13 @@ import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -95,7 +99,12 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(
|
||||
segDisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers
|
||||
) {
|
||||
const { SOPInstanceUID } = segDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
@@ -137,7 +146,12 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
|
||||
async function _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
}: withAppTypes) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
@@ -212,7 +226,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
// Todo: we need to pass in the button section Id since we are kind of
|
||||
// forcing the button to have black background since initially
|
||||
// it is designed for the toolbox not the toolbar on top
|
||||
// we should then branch the buttonSectionId to have different styles
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright !bg-black opacity-50',
|
||||
disabledText: disabledText ?? 'No segmentations available',
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName) && !toolNames) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isPrimaryActive = toolNames
|
||||
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
|
||||
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
|
||||
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
isActive: isPrimaryActive,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import preRegistration from './init';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||
@@ -28,8 +28,6 @@ const extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
preRegistration,
|
||||
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
@@ -38,8 +36,8 @@ const extension = {
|
||||
*/
|
||||
getPanelModule,
|
||||
getCommandsModule,
|
||||
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
getToolbarModule,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneSEGViewport
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { addTool, BrushTool } from '@cornerstonejs/tools';
|
||||
|
||||
export default function init({ configuration = {} }): void {
|
||||
addTool(BrushTool);
|
||||
}
|
||||
@@ -1,23 +1,39 @@
|
||||
import { createReportAsync } from '@ohif/extension-default';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTable } from '@ohif/ui';
|
||||
|
||||
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
|
||||
import { SegmentationPanelMode } from '../types/segmentation';
|
||||
import callInputDialog from './callInputDialog';
|
||||
import callColorPickerDialog from './colorPickerDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
const components = {
|
||||
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
||||
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
|
||||
};
|
||||
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}) {
|
||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
@@ -47,6 +63,64 @@ export default function PanelSegmentation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// temporary measure to not allow add segmentation when the selected viewport
|
||||
// is stack viewport
|
||||
useEffect(() => {
|
||||
const handleActiveViewportChange = viewportId => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||
viewportId || viewportGridService.getActiveViewportId()
|
||||
);
|
||||
|
||||
if (!displaySetUIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isReconstructable =
|
||||
displaySetUIDs?.some(displaySetUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
||||
return displaySet?.isReconstructable;
|
||||
}) || false;
|
||||
|
||||
if (isReconstructable) {
|
||||
setAddSegmentationClassName('');
|
||||
} else {
|
||||
setAddSegmentationClassName('ohif-disabled');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial state
|
||||
handleActiveViewportChange();
|
||||
|
||||
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
|
||||
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
|
||||
|
||||
const subsGrid = [];
|
||||
[ready, changedGrid].forEach(evt => {
|
||||
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
|
||||
handleActiveViewportChange(viewportId);
|
||||
});
|
||||
|
||||
subsGrid.push(unsubscribe);
|
||||
});
|
||||
|
||||
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const subsData = [];
|
||||
[changedData].forEach(evt => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
|
||||
handleActiveViewportChange();
|
||||
});
|
||||
|
||||
subsData.push(unsubscribe);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
subsGrid.forEach(unsub => unsub());
|
||||
subsData.forEach(unsub => unsub());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
@@ -54,7 +128,9 @@ export default function PanelSegmentation({
|
||||
};
|
||||
|
||||
const onSegmentationAdd = async () => {
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport');
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport', {
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationClick = (segmentationId: string) => {
|
||||
@@ -147,6 +223,7 @@ export default function PanelSegmentation({
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
// segment hide
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
@@ -170,6 +247,22 @@ export default function PanelSegmentation({
|
||||
|
||||
const onToggleSegmentationVisibility = segmentationId => {
|
||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const isVisible = segmentation.isVisible;
|
||||
const segments = segmentation.segments;
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segments.forEach((segment, segmentIndex) => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
isVisible,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const _setSegmentationConfiguration = useCallback(
|
||||
@@ -221,58 +314,84 @@ export default function PanelSegmentation({
|
||||
});
|
||||
};
|
||||
|
||||
const SegmentationGroupTableComponent =
|
||||
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
|
||||
const allowAddSegment = configuration?.addSegment;
|
||||
const onSegmentationAddWrapper =
|
||||
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
|
||||
? configuration?.onSegmentationAdd
|
||||
: onSegmentationAdd;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto">
|
||||
<SegmentationGroupTable
|
||||
title={t('Segmentations')}
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAdd}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
showDeleteSegment={true}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SegmentationGroupTableComponent
|
||||
title={t('Segmentations')}
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
showDeleteSegment={true}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState, useReducer } from 'react';
|
||||
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
|
||||
import { Types } from '@ohif/extension-cornerstone';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const TOOL_TYPES = {
|
||||
CIRCULAR_BRUSH: 'CircularBrush',
|
||||
SPHERE_BRUSH: 'SphereBrush',
|
||||
CIRCULAR_ERASER: 'CircularEraser',
|
||||
SPHERE_ERASER: 'SphereEraser',
|
||||
CIRCLE_SHAPE: 'CircleScissor',
|
||||
RECTANGLE_SHAPE: 'RectangleScissor',
|
||||
SPHERE_SHAPE: 'SphereScissor',
|
||||
THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush',
|
||||
THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush',
|
||||
};
|
||||
|
||||
const ACTIONS = {
|
||||
SET_TOOL_CONFIG: 'SET_TOOL_CONFIG',
|
||||
SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL',
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
Brush: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
|
||||
},
|
||||
Eraser: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
|
||||
},
|
||||
Shapes: {
|
||||
brushSize: 15,
|
||||
mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor'
|
||||
},
|
||||
ThresholdBrush: {
|
||||
brushSize: 15,
|
||||
thresholdRange: null,
|
||||
},
|
||||
activeTool: null,
|
||||
};
|
||||
|
||||
function toolboxReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case ACTIONS.SET_TOOL_CONFIG:
|
||||
const { tool, config } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[tool]: {
|
||||
...state[tool],
|
||||
...config,
|
||||
},
|
||||
};
|
||||
case ACTIONS.SET_ACTIVE_TOOL:
|
||||
return { ...state, activeTool: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function SegmentationToolbox({ servicesManager, extensionManager }) {
|
||||
const { toolbarService, segmentationService, toolGroupService } =
|
||||
servicesManager.services as Types.CornerstoneServices;
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const [toolsEnabled, setToolsEnabled] = useState(false);
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const updateActiveTool = useCallback(() => {
|
||||
if (!viewports?.size || activeViewportId === undefined) {
|
||||
return;
|
||||
}
|
||||
const viewport = viewports.get(activeViewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_ACTIVE_TOOL,
|
||||
payload: toolGroupService.getActiveToolForViewport(viewport.viewportId),
|
||||
});
|
||||
}, [activeViewportId, viewports, toolGroupService, dispatch]);
|
||||
|
||||
const setToolActive = useCallback(
|
||||
toolName => {
|
||||
initializeThresholdValue(toolName);
|
||||
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName });
|
||||
},
|
||||
[toolbarService, dispatch]
|
||||
);
|
||||
|
||||
/**
|
||||
* sets the tools enabled IF there are segmentations
|
||||
*/
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
];
|
||||
|
||||
const unsubscriptions = [];
|
||||
|
||||
events.forEach(event => {
|
||||
const { unsubscribe } = segmentationService.subscribe(event, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
const activeSegmentation = segmentations?.find(seg => seg.isActive);
|
||||
|
||||
setToolsEnabled(activeSegmentation?.segmentCount > 0);
|
||||
});
|
||||
|
||||
unsubscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
updateActiveTool();
|
||||
|
||||
return () => {
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
};
|
||||
}, [activeViewportId, viewports, segmentationService, updateActiveTool]);
|
||||
|
||||
/**
|
||||
* Update the active tool when the toolbar state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
updateActiveTool();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService, updateActiveTool]);
|
||||
|
||||
useEffect(() => {
|
||||
// if the active tool is not a brush tool then do nothing
|
||||
if (!Object.values(TOOL_TYPES).includes(state.activeTool)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is enabled then do nothing
|
||||
if (toolsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is disabled, then switch
|
||||
// back to the window level tool to not confuse the user when no
|
||||
// segmentation is active or when there is no segment in the segmentation
|
||||
setToolActive('WindowLevel');
|
||||
}, [toolsEnabled, state.activeTool, setToolActive]);
|
||||
|
||||
const updateBrushSize = useCallback(
|
||||
(toolName, brushSize) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
},
|
||||
[toolGroupService]
|
||||
);
|
||||
|
||||
function initializeThresholdValue(toolName: any) {
|
||||
if (state.ThresholdBrush.thresholdRange === null) {
|
||||
// set the default threshold range from the tool configuration
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
const toolGroupId = toolGroupIds[0];
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const toolConfig = toolGroup.getToolConfiguration(toolName);
|
||||
const defaultThresholdRange = toolConfig?.strategySpecificConfiguration?.THRESHOLD?.threshold;
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: 'ThresholdBrush',
|
||||
config: { thresholdRange: defaultThresholdRange },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const onBrushSizeChange = useCallback(
|
||||
(valueAsStringOrNumber, toolCategory) => {
|
||||
const value = Number(valueAsStringOrNumber);
|
||||
|
||||
_getToolNamesFromCategory(toolCategory).forEach(toolName => {
|
||||
updateBrushSize(toolName, value);
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: toolCategory,
|
||||
config: { brushSize: value },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch]
|
||||
);
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
newRange => {
|
||||
if (
|
||||
newRange[0] === state.ThresholdBrush.thresholdRange?.[0] &&
|
||||
newRange[1] === state.ThresholdBrush.thresholdRange?.[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolNames = _getToolNamesFromCategory('ThresholdBrush');
|
||||
|
||||
toolNames.forEach(toolName => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: newRange,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: 'ThresholdBrush',
|
||||
config: { thresholdRange: newRange },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch, state.ThresholdBrush.thresholdRange]
|
||||
);
|
||||
|
||||
return (
|
||||
<AdvancedToolbox
|
||||
title="Segmentation Tools"
|
||||
items={[
|
||||
{
|
||||
name: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'brush-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Brush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Brush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'brush-mode',
|
||||
value: state.Brush.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_ERASER ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_ERASER,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
type: 'range',
|
||||
id: 'eraser-radius',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Eraser.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Eraser'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: state.Eraser.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Shapes',
|
||||
icon: 'icon-tool-shape',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.RECTANGLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_SHAPE,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SHAPE),
|
||||
options: [
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
value: state.Shapes.mode,
|
||||
id: 'shape-mode',
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCLE_SHAPE, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.RECTANGLE_SHAPE, label: 'Rectangle' },
|
||||
{ value: TOOL_TYPES.SPHERE_SHAPE, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Threshold Tool',
|
||||
icon: 'icon-tool-threshold',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'threshold-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.ThresholdBrush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'ThresholdBrush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'threshold-mode',
|
||||
value: state.activeTool,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
id: 'segmentation-threshold-range',
|
||||
children: () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-secondary-light h-[1px]"></div>
|
||||
<div className="mt-1 text-[13px] text-white">Threshold</div>
|
||||
<InputDoubleRange
|
||||
values={state.ThresholdBrush.thresholdRange}
|
||||
onChange={handleRangeChange}
|
||||
minValue={-1000} // Todo: these should be configurable
|
||||
maxValue={1000}
|
||||
step={1}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getToolNamesFromCategory(category) {
|
||||
let toolNames = [];
|
||||
switch (category) {
|
||||
case 'Brush':
|
||||
toolNames = ['CircularBrush', 'SphereBrush'];
|
||||
break;
|
||||
case 'Eraser':
|
||||
toolNames = ['CircularEraser', 'SphereEraser'];
|
||||
break;
|
||||
case 'ThresholdBrush':
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return toolNames;
|
||||
}
|
||||
|
||||
export default SegmentationToolbox;
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SegmentationPanelMode {
|
||||
Expanded = 'expanded',
|
||||
Dropdown = 'dropdown',
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import { Enums, cache } from '@cornerstonejs/core';
|
||||
* @param params.viewportId - ID of the viewport to be updated.
|
||||
* @param params.loadFn - Function to load the segmentation data.
|
||||
* @param params.servicesManager - The services manager.
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
* @param params.displaySet - the display set.
|
||||
* @param params.initialSliceIndex - The initial slice index.
|
||||
*
|
||||
* @returns Returns true upon successful update of viewports for segmentation rendering.
|
||||
*/
|
||||
@@ -18,12 +19,14 @@ async function updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
loadFn,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
displaySet,
|
||||
initialSliceIndex = null,
|
||||
}: {
|
||||
viewportId: string;
|
||||
loadFn: () => Promise<string>;
|
||||
servicesManager: any;
|
||||
referencedDisplaySetInstanceUID?: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
displaySet?: any;
|
||||
initialSliceIndex?: number;
|
||||
}) {
|
||||
const { cornerstoneViewportService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
@@ -31,13 +34,13 @@ async function updateViewportsForSegmentationRendering({
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
const referencedDisplaySetInstanceUID =
|
||||
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
|
||||
const updatedViewports = getUpdatedViewportsForSegmentation({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
referencedDisplaySetInstanceUID,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
});
|
||||
|
||||
// create Segmentation callback which needs to be waited until
|
||||
@@ -56,17 +59,25 @@ async function updateViewportsForSegmentationRendering({
|
||||
updatedViewports.forEach(async viewport => {
|
||||
viewport.viewportOptions = {
|
||||
...viewport.viewportOptions,
|
||||
viewportType: 'volume',
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
};
|
||||
const viewportId = viewport.viewportId;
|
||||
|
||||
// maintain the prehydration slice on the target viewport only
|
||||
if (viewportId === targetViewportId) {
|
||||
viewport.viewportOptions.initialImageOptions = {
|
||||
index: initialSliceIndex,
|
||||
useOnce: true,
|
||||
};
|
||||
}
|
||||
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const prevCamera = csViewport.getCamera();
|
||||
|
||||
// only run the createSegmentationForVolume for the targetViewportId
|
||||
// since the rest will get handled by cornerstoneViewportService
|
||||
if (volumeExists && viewportId === targetViewportId) {
|
||||
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
|
||||
await createSegmentationForVolume();
|
||||
return;
|
||||
}
|
||||
@@ -126,19 +137,19 @@ const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the ID of the viewport to be updated.
|
||||
* @param params.servicesManager - The services manager
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
* @param params.displaySet - the display set.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
}) {
|
||||
displaySet,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
@@ -146,14 +157,15 @@ function getUpdatedViewportsForSegmentation({
|
||||
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
|
||||
|
||||
const referenceDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
|
||||
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
targetViewportId,
|
||||
referenceDisplaySetInstanceUID
|
||||
referenceDisplaySetInstanceUID,
|
||||
isHangingProtocolLayout
|
||||
);
|
||||
|
||||
viewports.forEach((viewport, viewportId) => {
|
||||
@@ -174,13 +186,14 @@ function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return updatedViewports;
|
||||
|
||||
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -12,23 +12,29 @@ function promptHydrateSEG({
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateSEGDisplaySet,
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
const extensionManager = servicesManager._extensionManager;
|
||||
const appConfig = extensionManager._appConfig;
|
||||
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
|
||||
const promptResult = appConfig?.disableConfirmationPrompts
|
||||
? RESPONSE.HYDRATE_SEG
|
||||
: await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
window.setTimeout(async () => {
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
resolve(isHydrated);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -38,11 +44,13 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
@@ -63,6 +71,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
function OHIFCornerstoneSEGViewport(props) {
|
||||
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
@@ -28,8 +25,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
displaySetService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
@@ -114,16 +111,17 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
// initialImageIndex={initialImageIndex}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -248,6 +246,69 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
const hydrateSEGDisplaySet = useCallback(
|
||||
({ segDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [segDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
@@ -283,61 +344,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
SpacingBetweenSlices,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const hydrateSEGDisplaySet = ({ segDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [segDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
const onStatusClick = async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onSegmentChange}
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
|
||||
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined ? utils.roundNumber(SpacingBetweenSlices, 2) : '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{segIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
@@ -359,11 +367,6 @@ OHIFCornerstoneSEGViewport.propTypes = {
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
OHIFCornerstoneSEGViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
||||
|
||||
@@ -6,9 +6,6 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
@@ -26,23 +23,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.8.0-beta.53",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -23,6 +23,8 @@
|
||||
"ohif-extension"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -32,21 +34,21 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.0-beta.53",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.53",
|
||||
"@ohif/extension-measurement-tracking": "3.8.0-beta.53",
|
||||
"@ohif/ui": "3.8.0-beta.53",
|
||||
"dcmjs": "^0.29.12",
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.97",
|
||||
"@ohif/extension-measurement-tracking": "3.9.0-beta.97",
|
||||
"@ohif/ui": "3.9.0-beta.97",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2"
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.44.1",
|
||||
"@cornerstonejs/core": "^1.44.1",
|
||||
"@cornerstonejs/tools": "^1.44.1",
|
||||
"@cornerstonejs/adapters": "^1.82.4",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -40,7 +40,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
||||
return dataset;
|
||||
};
|
||||
|
||||
const commandsModule = props => {
|
||||
const commandsModule = (props: withAppTypes) => {
|
||||
const { servicesManager } = props;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const actions = {
|
||||
@@ -57,7 +57,7 @@ const commandsModule = props => {
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
},
|
||||
|
||||
@@ -128,13 +128,9 @@ const commandsModule = props => {
|
||||
const definitions = {
|
||||
downloadReport: {
|
||||
commandFn: actions.downloadReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
storeMeasurements: {
|
||||
commandFn: actions.storeMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { OHIFCornerstoneSRContentItem } from './OHIFCornerstoneSRContentItem';
|
||||
|
||||
export function OHIFCornerstoneSRContainer(props) {
|
||||
const { container, nodeIndexesTree = [0], containerNumberedTree = [1] } = props;
|
||||
const { ContinuityOfContent, ConceptNameCodeSequence } = container;
|
||||
const { CodeMeaning } = ConceptNameCodeSequence ?? {};
|
||||
let childContainerIndex = 1;
|
||||
const contentItems = container.ContentSequence?.map((contentItem, i) => {
|
||||
const { ValueType } = contentItem;
|
||||
const childNodeLevel = [...nodeIndexesTree, i];
|
||||
const key = childNodeLevel.join('.');
|
||||
|
||||
let Component;
|
||||
let componentProps;
|
||||
|
||||
if (ValueType === 'CONTAINER') {
|
||||
const childContainerNumberedTree = [...containerNumberedTree, childContainerIndex++];
|
||||
|
||||
Component = OHIFCornerstoneSRContainer;
|
||||
componentProps = {
|
||||
container: contentItem,
|
||||
nodeIndexesTree: childNodeLevel,
|
||||
containerNumberedTree: childContainerNumberedTree,
|
||||
};
|
||||
} else {
|
||||
Component = OHIFCornerstoneSRContentItem;
|
||||
componentProps = {
|
||||
contentItem,
|
||||
nodeIndexesTree: childNodeLevel,
|
||||
continuityOfContent: ContinuityOfContent,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
key={key}
|
||||
{...componentProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-bold">
|
||||
{containerNumberedTree.join('.')}.
|
||||
{CodeMeaning}
|
||||
</div>
|
||||
<div className="ml-4 mb-2">{contentItems}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSRContainer.propTypes = {
|
||||
/**
|
||||
* A tree node that may contain another container or one or more content items
|
||||
* (text, code, uidref, pname, etc.)
|
||||
*/
|
||||
container: PropTypes.object,
|
||||
/**
|
||||
* A 0-based index list
|
||||
*/
|
||||
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
|
||||
/**
|
||||
* A 1-based index list that represents a container in a multi-level numbered
|
||||
* list (tree).
|
||||
*
|
||||
* Example:
|
||||
* 1. History
|
||||
* 1.1. Chief Complaint
|
||||
* 1.2. Present Illness
|
||||
* 1.3. Past History
|
||||
* 1.4. Family History
|
||||
* 2. Findings
|
||||
* */
|
||||
containerNumberedTree: PropTypes.arrayOf(PropTypes.number),
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CodeNameCodeSequenceValues from '../constants/CodeNameCodeSequenceValues';
|
||||
import formatContentItemValue from '../utils/formatContentItem';
|
||||
|
||||
const EMPTY_TAG_VALUE = '[empty]';
|
||||
|
||||
function OHIFCornerstoneSRContentItem(props) {
|
||||
const { contentItem, nodeIndexesTree, continuityOfContent } = props;
|
||||
const { ConceptNameCodeSequence } = contentItem;
|
||||
const { CodeValue, CodeMeaning } = ConceptNameCodeSequence;
|
||||
const isChildFirstNode = nodeIndexesTree[nodeIndexesTree.length - 1] === 0;
|
||||
const formattedValue = formatContentItemValue(contentItem) ?? EMPTY_TAG_VALUE;
|
||||
const startWithAlphaNumCharRegEx = /^[a-zA-Z0-9]/;
|
||||
const isContinuous = continuityOfContent === 'CONTINUOUS';
|
||||
const isFinding = CodeValue === CodeNameCodeSequenceValues.Finding;
|
||||
const addExtraSpace =
|
||||
isContinuous && !isChildFirstNode && startWithAlphaNumCharRegEx.test(formattedValue?.[0]);
|
||||
|
||||
// Collapse sequences of white space preserving newline characters
|
||||
let className = 'whitespace-pre-line';
|
||||
|
||||
if (CodeValue === CodeNameCodeSequenceValues.Finding) {
|
||||
// Preserve spaces because it is common to see tabular text in a
|
||||
// "Findings" ConceptNameCodeSequence
|
||||
className = 'whitespace-pre-wrap';
|
||||
}
|
||||
|
||||
if (isContinuous) {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={className}
|
||||
title={CodeMeaning}
|
||||
>
|
||||
{addExtraSpace ? ' ' : ''}
|
||||
{formattedValue}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="font-bold">{CodeMeaning}: </span>
|
||||
{isFinding ? (
|
||||
<pre>{formattedValue}</pre>
|
||||
) : (
|
||||
<span className={className}>{formattedValue}</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSRContentItem.propTypes = {
|
||||
contentItem: PropTypes.object,
|
||||
nodeIndexesTree: PropTypes.arrayOf(PropTypes.number),
|
||||
continuityOfContent: PropTypes.string,
|
||||
};
|
||||
|
||||
export { OHIFCornerstoneSRContentItem };
|
||||
+97
-105
@@ -1,35 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
const { formatDate } = utils;
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRViewport(props) {
|
||||
const {
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportLabel,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { displaySetService, cornerstoneViewportService, measurementService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
measurementService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
@@ -48,6 +44,8 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
const [element, setElement] = useState(null);
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
|
||||
// Optional hook into tracking extension, if present.
|
||||
let trackedMeasurements;
|
||||
let sendTrackedMeasurementsEvent;
|
||||
@@ -128,6 +126,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||
}
|
||||
|
||||
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
_getViewportReferencedDisplaySetData(
|
||||
srDisplaySet,
|
||||
newMeasurementSelected,
|
||||
@@ -202,7 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||
positionIds: null,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
@@ -213,18 +218,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
direction => {
|
||||
let newMeasurementSelected = measurementSelected;
|
||||
|
||||
if (direction === 'right') {
|
||||
newMeasurementSelected++;
|
||||
|
||||
if (newMeasurementSelected >= measurementCount) {
|
||||
newMeasurementSelected = 0;
|
||||
}
|
||||
} else {
|
||||
newMeasurementSelected--;
|
||||
|
||||
if (newMeasurementSelected < 0) {
|
||||
newMeasurementSelected = measurementCount - 1;
|
||||
}
|
||||
newMeasurementSelected += direction;
|
||||
if (newMeasurementSelected >= measurementCount) {
|
||||
newMeasurementSelected = 0;
|
||||
} else if (newMeasurementSelected < 0) {
|
||||
newMeasurementSelected = measurementCount - 1;
|
||||
}
|
||||
|
||||
setTrackingIdentifiers(newMeasurementSelected);
|
||||
@@ -263,11 +261,15 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* if it is hydrated we don't even use the SR viewport.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
const loadSR = async () => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
await srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
updateViewport(measurementSelected);
|
||||
};
|
||||
loadSR();
|
||||
}, [srDisplaySet]);
|
||||
|
||||
/**
|
||||
@@ -275,28 +277,66 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* the element changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!element || !srDisplaySet.isLoaded) {
|
||||
return;
|
||||
}
|
||||
setTrackingIdentifiers(measurementSelected);
|
||||
const updateSR = async () => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
await srDisplaySet.load();
|
||||
}
|
||||
if (!element || !srDisplaySet.isLoaded) {
|
||||
return;
|
||||
}
|
||||
setTrackingIdentifiers(measurementSelected);
|
||||
};
|
||||
updateSR();
|
||||
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
||||
|
||||
/**
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
useEffect(() => {
|
||||
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
}, [trackedMeasurements]);
|
||||
|
||||
/**
|
||||
* Data fetching for the SR displaySet, which updates the measurements and
|
||||
* also gets the referenced image displaySet that SR is based on.
|
||||
*/
|
||||
useEffect(() => {
|
||||
updateViewport(measurementSelected);
|
||||
}, [dataSource, srDisplaySet]);
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportId,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
index: 0,
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onMeasurementChange}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
isLocked,
|
||||
onMeasurementChange,
|
||||
sendTrackedMeasurementsEvent,
|
||||
srDisplaySet,
|
||||
t,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
@@ -317,57 +357,8 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetMetadata;
|
||||
|
||||
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onMeasurementChange}
|
||||
getStatusComponent={() =>
|
||||
_getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportId,
|
||||
isTracked: false,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
})
|
||||
}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined ? `${SpacingBetweenSlices.toFixed(2)}mm` : '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{getCornerstoneViewport()}
|
||||
{childrenWithProps}
|
||||
@@ -376,23 +367,17 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSRViewport.propTypes = {
|
||||
OHIFCornerstoneSRMeasurementViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
customProps: PropTypes.object,
|
||||
viewportOptions: PropTypes.object,
|
||||
viewportLabel: PropTypes.string,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
OHIFCornerstoneSRViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
async function _getViewportReferencedDisplaySetData(
|
||||
displaySet,
|
||||
measurementSelected,
|
||||
@@ -402,6 +387,13 @@ async function _getViewportReferencedDisplaySetData(
|
||||
const measurement = measurements[measurementSelected];
|
||||
|
||||
const { displaySetInstanceUID } = measurement;
|
||||
if (!displaySet.keyImageDisplaySet) {
|
||||
// Create a new display set, and preserve a reference to it here,
|
||||
// so that it can be re-displayed and shown inside the SR viewport.
|
||||
// This is only for ease of redisplay - the display set is stored in the
|
||||
// usual manner in the display set service.
|
||||
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
@@ -429,6 +421,7 @@ function _getStatusComponent({
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}) {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
@@ -437,7 +430,6 @@ function _getStatusComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
@@ -516,4 +508,4 @@ function _getStatusComponent({
|
||||
);
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneSRViewport;
|
||||
export default OHIFCornerstoneSRMeasurementViewport;
|
||||
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
import { OHIFCornerstoneSRContainer } from './OHIFCornerstoneSRContainer';
|
||||
|
||||
function OHIFCornerstoneSRTextViewport(props: withAppTypes) {
|
||||
const { displaySets } = props;
|
||||
const displaySet = displaySets[0];
|
||||
const instance = displaySet.instances[0];
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-auto p-4 text-white">
|
||||
<div>
|
||||
{/* The root level is always a container */}
|
||||
<OHIFCornerstoneSRContainer container={instance} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSRTextViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
viewportOptions: PropTypes.object,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
export default OHIFCornerstoneSRTextViewport;
|
||||
@@ -0,0 +1,30 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
import OHIFCornerstoneSRMeasurementViewport from './OHIFCornerstoneSRMeasurementViewport';
|
||||
import OHIFCornerstoneSRTextViewport from './OHIFCornerstoneSRTextViewport';
|
||||
|
||||
function OHIFCornerstoneSRViewport(props: withAppTypes) {
|
||||
const { displaySets } = props;
|
||||
const { isImagingMeasurementReport } = displaySets[0];
|
||||
|
||||
if (isImagingMeasurementReport) {
|
||||
return <OHIFCornerstoneSRMeasurementViewport {...props}></OHIFCornerstoneSRMeasurementViewport>;
|
||||
}
|
||||
|
||||
return <OHIFCornerstoneSRTextViewport {...props}></OHIFCornerstoneSRTextViewport>;
|
||||
}
|
||||
|
||||
OHIFCornerstoneSRViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
viewportOptions: PropTypes.object,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
export default OHIFCornerstoneSRViewport;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
const CodeNameCodeSequenceValues = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
ImagingMeasurements: '126010',
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
|
||||
};
|
||||
|
||||
export { CodeNameCodeSequenceValues as default };
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
import CodeNameCodeSequenceValues from './constants/CodeNameCodeSequenceValues';
|
||||
|
||||
type InstanceMetadata = Types.InstanceMetadata;
|
||||
|
||||
@@ -16,10 +17,11 @@ const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
|
||||
|
||||
const sopClassUids = [
|
||||
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
|
||||
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
|
||||
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
||||
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
|
||||
'1.2.840.10008.5.1.4.1.1.88.11', // BASIC TEXT SR
|
||||
'1.2.840.10008.5.1.4.1.1.88.22', // ENHANCED SR
|
||||
'1.2.840.10008.5.1.4.1.1.88.33', // COMPREHENSIVE SR
|
||||
'1.2.840.10008.5.1.4.1.1.88.34', // Comprehensive 3D SR
|
||||
// '1.2.840.10008.5.1.4.1.1.88.50', // Mammography CAD SR
|
||||
];
|
||||
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||
@@ -34,19 +36,6 @@ const validateSameStudyUID = (uid: string, instances): void => {
|
||||
});
|
||||
};
|
||||
|
||||
const CodeNameCodeSequenceValues = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
ImagingMeasurements: '126010',
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
|
||||
};
|
||||
|
||||
const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
|
||||
@@ -85,7 +74,11 @@ function addInstances(instances: InstanceMetadata[], displaySetService: DisplayS
|
||||
* @param servicesManager is the services that can be used for creating
|
||||
* @returns The list of display sets created for the given instances object
|
||||
*/
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
// If the series has no instances, stop here
|
||||
if (!instances || !instances.length) {
|
||||
throw new Error('No instances were provided');
|
||||
@@ -109,22 +102,10 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
} = instance;
|
||||
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
||||
|
||||
if (
|
||||
!ConceptNameCodeSequence ||
|
||||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
) {
|
||||
servicesManager.services.uiNotificationService.show({
|
||||
title: 'DICOM SR',
|
||||
message:
|
||||
'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
|
||||
type: 'warning',
|
||||
duration: 6000,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
const isImagingMeasurementReport =
|
||||
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
|
||||
|
||||
const displaySet = {
|
||||
//plugin: id,
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
@@ -140,6 +121,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
measurements: null,
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
isImagingMeasurementReport,
|
||||
sopClassUids,
|
||||
instance,
|
||||
addInstances,
|
||||
@@ -150,15 +132,43 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(displaySet, servicesManager, extensionManager) {
|
||||
async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) {
|
||||
const { displaySetService, measurementService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
const { ContentSequence } = displaySet.instance;
|
||||
|
||||
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
async function retrieveBulkData(obj, parentObj = null, key = null) {
|
||||
for (const prop in obj) {
|
||||
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
|
||||
await retrieveBulkData(obj[prop], obj, prop);
|
||||
} else if (Array.isArray(obj[prop])) {
|
||||
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
|
||||
} else if (prop === 'BulkDataURI') {
|
||||
const value = await dataSource.retrieve.bulkDataURI({
|
||||
BulkDataURI: obj[prop],
|
||||
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
|
||||
});
|
||||
if (parentObj && key) {
|
||||
parentObj[key] = new Float32Array(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displaySet.isLoaded !== true) {
|
||||
await retrieveBulkData(ContentSequence);
|
||||
}
|
||||
|
||||
if (displaySet.isImagingMeasurementReport) {
|
||||
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
} else {
|
||||
displaySet.referencedImages = [];
|
||||
displaySet.measurements = [];
|
||||
}
|
||||
|
||||
const mappings = measurementService.getSourceMappings(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
@@ -199,7 +209,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource,
|
||||
servicesManager
|
||||
servicesManager: AppTypes.ServicesManager
|
||||
) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
@@ -211,7 +221,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newDisplaySet instanceof ImageSet) {
|
||||
if ((!newDisplaySet) instanceof ImageSet) {
|
||||
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||
return;
|
||||
}
|
||||
@@ -275,7 +285,22 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
}
|
||||
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
|
||||
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
|
||||
const frame =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
|
||||
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
|
||||
|
||||
/** Update measurement properties */
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frame;
|
||||
delete measurement.coords;
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
@@ -291,7 +316,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
||||
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||
const ReferencedFrameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
||||
@@ -328,6 +353,10 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
|
||||
);
|
||||
|
||||
if (!ImagingMeasurements) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
|
||||
);
|
||||
@@ -582,6 +611,10 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
|
||||
);
|
||||
|
||||
if (!ImageLibrary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
|
||||
import { srProtocol } from './getHangingProtocolModule';
|
||||
import onModeEnter from './onModeEnter';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import preRegistration from './init';
|
||||
@@ -10,7 +10,7 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
|
||||
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSRViewport = props => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
addTool,
|
||||
AngleTool,
|
||||
annotation,
|
||||
ArrowAnnotateTool,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
RectangleROITool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import addToolInstance from './utils/addToolInstance';
|
||||
@@ -19,24 +19,25 @@ import toolNames from './tools/toolNames';
|
||||
* @param {object} configuration
|
||||
*/
|
||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||
addTool(DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool);
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
|
||||
|
||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
||||
// TODO - fix the rehydration of Freehand, as it throws an exception
|
||||
// on a missing polyline. The fix is probably in CS3D
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const dashedLine = {
|
||||
lineDash: '4,4',
|
||||
};
|
||||
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
||||
[toolNames.DICOMSRDisplay]: dashedLine,
|
||||
SRLength: dashedLine,
|
||||
SRBidirectional: dashedLine,
|
||||
SREllipticalROI: dashedLine,
|
||||
@@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
SRPlanarFreehandROI: dashedLine,
|
||||
SRRectangleROI: dashedLine,
|
||||
global: {},
|
||||
});
|
||||
}
|
||||
@@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
}
|
||||
|
||||
_getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 3);
|
||||
const labelLength = Math.min(labels.length, 5);
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < labelLength; i++) {
|
||||
const labelEntry = labels[i];
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
@@ -65,7 +65,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredAnnotations = annotations.filter(annotation =>
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.cachedStats?.TrackingUniqueIdentifier)
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
if (!viewport._actors?.size) {
|
||||
@@ -77,20 +77,24 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
toolName: this.getToolName(),
|
||||
viewportId: enabledElement.viewport.id,
|
||||
};
|
||||
const { style: annotationStyle } = annotation.config;
|
||||
|
||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||
const annotation = filteredAnnotations[i];
|
||||
const annotationUID = annotation.annotationUID;
|
||||
const { renderableData } = annotation.data.cachedStats;
|
||||
const { cachedStats } = annotation.data;
|
||||
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
|
||||
const { referencedImageId } = annotation.metadata;
|
||||
|
||||
styleSpecifier.annotationUID = annotationUID;
|
||||
|
||||
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
|
||||
this.getToolName()
|
||||
];
|
||||
|
||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||
const color =
|
||||
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
? 'rgb(0, 255, 0)'
|
||||
: this.getStyle('color', styleSpecifier, annotation);
|
||||
|
||||
@@ -98,6 +102,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
color,
|
||||
lineDash,
|
||||
lineWidth,
|
||||
...groupStyle,
|
||||
};
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
@@ -160,6 +165,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
const drawingOptions = {
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
};
|
||||
let allCanvasCoordinates = [];
|
||||
renderableData.map((data, index) => {
|
||||
@@ -307,6 +313,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
{
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
+18
-36
@@ -1,15 +1,13 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import { Types, annotation } from '@cornerstonejs/tools';
|
||||
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import toolNames from '../tools/toolNames';
|
||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
|
||||
const toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
const measurementData = {
|
||||
@@ -27,26 +25,25 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
|
||||
}
|
||||
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
|
||||
_getRenderableData(GraphicType, GraphicData, imageId)
|
||||
);
|
||||
});
|
||||
|
||||
// Use the metadata provider to grab its imagePlaneModule metadata
|
||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
|
||||
// Create Cornerstone3D Annotation from measurement
|
||||
const frameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/**
|
||||
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
|
||||
* This is used before the annotation is hydrated. If hydrated the measurement will be added
|
||||
* to the measurement service and will be available for the other viewports.
|
||||
*/
|
||||
const SRAnnotation: Types.Annotation = {
|
||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||
highlighted: false,
|
||||
isLocked: false,
|
||||
invalidated: false,
|
||||
metadata: {
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
toolName: toolName,
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
@@ -54,32 +51,17 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
|
||||
handles: {
|
||||
textBox: measurement.textBox ?? {},
|
||||
},
|
||||
cachedStats: {
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
},
|
||||
frameNumber: frameNumber,
|
||||
cachedStats: {},
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
frameNumber,
|
||||
},
|
||||
};
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
annotationManager.addAnnotation(SRAnnotation);
|
||||
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||
|
||||
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||
// It'd be super weird if it didn't anyway as a SCOORD.
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frameNumber;
|
||||
delete measurement.coords;
|
||||
}
|
||||
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
|
||||
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId) {
|
||||
let renderableData: csTypes.Point3[];
|
||||
|
||||
switch (GraphicType) {
|
||||
@@ -0,0 +1,67 @@
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Formatters used to format each of the content items (SR "nodes") which can be
|
||||
* text, code, UID ref, number, person name, date, time and date time. Each
|
||||
* formatter must be a function with the following signature:
|
||||
*
|
||||
* [VALUE_TYPE]: (contentItem) => string
|
||||
*
|
||||
*/
|
||||
const contentItemFormatters = {
|
||||
TEXT: contentItem => contentItem.TextValue,
|
||||
CODE: contentItem => contentItem.ConceptCodeSequence?.[0]?.CodeMeaning,
|
||||
UIDREF: contentItem => contentItem.UID,
|
||||
NUM: contentItem => {
|
||||
const measuredValue = contentItem.MeasuredValueSequence?.[0];
|
||||
|
||||
if (!measuredValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { NumericValue, MeasurementUnitsCodeSequence } = measuredValue;
|
||||
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||
|
||||
return `${NumericValue} ${CodeValue}`;
|
||||
},
|
||||
PNAME: contentItem => {
|
||||
const personName = contentItem.PersonName?.[0]?.Alphabetic;
|
||||
return personName ? utils.formatPN(personName) : undefined;
|
||||
},
|
||||
DATE: contentItem => {
|
||||
const { Date } = contentItem;
|
||||
return Date ? utils.formatDate(Date) : undefined;
|
||||
},
|
||||
TIME: contentItem => {
|
||||
const { Time } = contentItem;
|
||||
return Time ? utils.formatTime(Time) : undefined;
|
||||
},
|
||||
DATETIME: contentItem => {
|
||||
const { DateTime } = contentItem;
|
||||
|
||||
if (typeof DateTime !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 14 characters because it should be something like 20180614113714
|
||||
if (DateTime.length < 14) {
|
||||
return DateTime;
|
||||
}
|
||||
|
||||
const dicomDate = DateTime.substring(0, 8);
|
||||
const dicomTime = DateTime.substring(8, 14);
|
||||
const formattedDate = utils.formatDate(dicomDate);
|
||||
const formattedTime = utils.formatTime(dicomTime);
|
||||
|
||||
return `${formattedDate} ${formattedTime}`;
|
||||
},
|
||||
};
|
||||
|
||||
function formatContentItemValue(contentItem) {
|
||||
const { ValueType } = contentItem;
|
||||
const fnFormat = contentItemFormatters[ValueType];
|
||||
|
||||
return fnFormat ? fnFormat(contentItem) : `[${ValueType} is not supported]`;
|
||||
}
|
||||
|
||||
export { formatContentItemValue as default, formatContentItemValue };
|
||||
+1
-1
@@ -43,7 +43,7 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '../');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.ts`,
|
||||
};
|
||||
|
||||
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
|
||||
return merge(commonConfig, {
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'ohif-extension-cornerstone',
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 cornerstone-dynamic-volume ()
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
# cornerstone-dynamic-volume
|
||||
## Description
|
||||
|
||||
## Author
|
||||
OHIF
|
||||
|
||||
## License
|
||||
MIT
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "OHIF extension for 4D volumes data",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
|
||||
"module": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./types": "./src/types/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/extension-cornerstone": "3.9.0-beta.97",
|
||||
"@ohif/extension-default": "3.9.0-beta.97",
|
||||
"@ohif/i18n": "3.9.0-beta.97",
|
||||
"@ohif/ui": "3.9.0-beta.97",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||
|
||||
export { updateSegmentationsChartDisplaySet };
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
|
||||
import * as cs from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
|
||||
const CHART_MODALITY = 'CHT';
|
||||
const SEG_CHART_INSTANCE_UID = utils.guid();
|
||||
|
||||
// Private SOPClassUid for chart data
|
||||
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
|
||||
|
||||
const { utilities: csToolsUtils } = csTools;
|
||||
|
||||
function _getDateTimeStr() {
|
||||
const now = new Date();
|
||||
const date =
|
||||
now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2);
|
||||
const time =
|
||||
('0' + now.getUTCHours()).slice(-2) +
|
||||
('0' + now.getUTCMinutes()).slice(-2) +
|
||||
('0' + now.getUTCSeconds()).slice(-2);
|
||||
|
||||
return { date, time };
|
||||
}
|
||||
|
||||
function _getTimePointsDataByTagName(volume, timePointsTag) {
|
||||
const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => {
|
||||
const instance = DicomMetadataStore.getInstanceByImageId(imageId);
|
||||
const timePointValue = instance[timePointsTag];
|
||||
|
||||
if (timePointValue !== undefined) {
|
||||
timePoints.add(timePointValue);
|
||||
}
|
||||
|
||||
return timePoints;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b);
|
||||
}
|
||||
|
||||
function _convertTimePointsUnit(timePoints, timePointsUnit) {
|
||||
const validUnits = ['ms', 's', 'm', 'h'];
|
||||
const divisors = [1000, 60, 60];
|
||||
const currentUnitIndex = validUnits.indexOf(timePointsUnit);
|
||||
let divisor = 1;
|
||||
|
||||
if (currentUnitIndex !== -1) {
|
||||
for (let i = currentUnitIndex; i < validUnits.length - 1; i++) {
|
||||
const newDivisor = divisor * divisors[i];
|
||||
const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length;
|
||||
|
||||
// Change the scale only if more than 50% of the time points are
|
||||
// greater than the new divisor.
|
||||
if (greaterThanDivisorCount <= timePoints.length / 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
divisor = newDivisor;
|
||||
timePointsUnit = validUnits[i + 1];
|
||||
}
|
||||
|
||||
if (divisor > 1) {
|
||||
timePoints = timePoints.map(timePoint => timePoint / divisor);
|
||||
}
|
||||
}
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
// It currently supports only one tag but a few other will be added soon
|
||||
// Supported 4D Tags
|
||||
// (0018,1060) Trigger Time [NOK]
|
||||
// (0018,0081) Echo Time [NOK]
|
||||
// (0018,0086) Echo Number [NOK]
|
||||
// (0020,0100) Temporal Position Identifier [NOK]
|
||||
// (0054,1300) FrameReferenceTime [OK]
|
||||
function _getTimePointsData(volume) {
|
||||
const timePointsTags = {
|
||||
FrameReferenceTime: {
|
||||
unit: 'ms',
|
||||
},
|
||||
};
|
||||
|
||||
const timePointsTagNames = Object.keys(timePointsTags);
|
||||
let timePoints;
|
||||
let timePointsUnit;
|
||||
|
||||
for (let i = 0; i < timePointsTagNames.length; i++) {
|
||||
const tagName = timePointsTagNames[i];
|
||||
const curTimePoints = _getTimePointsDataByTagName(volume, tagName);
|
||||
|
||||
if (curTimePoints.length) {
|
||||
timePoints = curTimePoints;
|
||||
timePointsUnit = timePointsTags[tagName].unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!timePoints.length) {
|
||||
const concatTagNames = timePointsTagNames.join(', ');
|
||||
|
||||
throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`);
|
||||
}
|
||||
|
||||
const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit);
|
||||
|
||||
timePoints = convertedTimePoints.timePoints;
|
||||
timePointsUnit = convertedTimePoints.timePointsUnit;
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID);
|
||||
let volumeCacheKey: string | undefined;
|
||||
const volumeId = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
for (const [key] of cs.cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let referencedDynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId(
|
||||
referencedDynamicVolume.imageIds[0]
|
||||
);
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
|
||||
if (pixelCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// since we only use one segmentation representation per segmentationId
|
||||
// it is fine to pick the first one
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||
segmentation.id
|
||||
);
|
||||
|
||||
const segmentationRepresentationUID =
|
||||
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||
|
||||
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||
segmentationRepresentationUID
|
||||
);
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
1 // segmentIndex
|
||||
);
|
||||
|
||||
const hexColor = cs.utilities.color.rgbToHex(...color);
|
||||
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||
|
||||
if (!timePointsData) {
|
||||
timePointsData = _getTimePointsData(referencedDynamicVolume);
|
||||
volumesTimePointsCache.set(referencedDynamicVolume, timePointsData);
|
||||
}
|
||||
|
||||
const { timePoints, timePointsUnit } = timePointsData;
|
||||
|
||||
if (timePoints.length !== timeData[0].length) {
|
||||
throw new Error('Invalid number of time points returned');
|
||||
}
|
||||
|
||||
const timepointsCount = timePoints.length;
|
||||
const chartSeriesData = new Array(timepointsCount);
|
||||
|
||||
for (let i = 0; i < timepointsCount; i++) {
|
||||
const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0);
|
||||
|
||||
chartSeriesData[i] = [timePoints[i], average];
|
||||
}
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
StudyDescription,
|
||||
chartData: {
|
||||
series: {
|
||||
label: segmentation.label,
|
||||
points: chartSeriesData,
|
||||
color: hexColor,
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
label: `Time (${timePointsUnit})`,
|
||||
},
|
||||
y: {
|
||||
label: `Vl (Bq/ml)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||
);
|
||||
|
||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||
const series = segmentationsData.reduce((allSeries, curSegData) => {
|
||||
return [...allSeries, curSegData.chartData.series];
|
||||
}, []);
|
||||
|
||||
const instance = {
|
||||
SOPClassUID: ChartDataSOPClassUid,
|
||||
Modality: CHART_MODALITY,
|
||||
SOPInstanceUID: utils.guid(),
|
||||
SeriesDate: seriesDate,
|
||||
SeriesTime: seriesTime,
|
||||
SeriesInstanceUID: SEG_CHART_INSTANCE_UID,
|
||||
StudyInstanceUID: segmentationsData[0].StudyInstanceUID,
|
||||
StudyDescription: segmentationsData[0].StudyDescription,
|
||||
SeriesNumber: 100,
|
||||
SeriesDescription: 'Segmentation chart series data',
|
||||
chartData: {
|
||||
series,
|
||||
axis: { ...segmentationsData[0].chartData.axis },
|
||||
},
|
||||
};
|
||||
|
||||
const seriesMetadata = {
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
Modality: instance.Modality,
|
||||
};
|
||||
|
||||
return { seriesMetadata, instance };
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
const { segmentationService, displaySetService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||
|
||||
if (seriesMetadata && instance) {
|
||||
// An event is triggered after adding the instance and the displaySet is created
|
||||
DicomMetadataStore.addSeriesMetadata([seriesMetadata], true);
|
||||
DicomMetadataStore.addInstances([instance], true);
|
||||
}
|
||||
}
|
||||
|
||||
export { updateSegmentationsChartDisplaySet as default };
|
||||
@@ -0,0 +1,407 @@
|
||||
import * as importedActions from './actions';
|
||||
import { utilities, Enums } from '@cornerstonejs/tools';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const services = servicesManager.services;
|
||||
const { displaySetService, viewportGridService, segmentationService } = services;
|
||||
|
||||
const actions = {
|
||||
...importedActions,
|
||||
getDynamic4DDisplaySet: () => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined ||
|
||||
anInstance.NumberOfTimeSlices !== undefined ||
|
||||
anInstance.TemporalPositionIdentifier !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return dynamic4DDisplaySet;
|
||||
},
|
||||
getComputedDisplaySets: () => {
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySets = [...displaySetCache.values()];
|
||||
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
|
||||
return displaySet.isDerived;
|
||||
});
|
||||
return computedDisplaySets;
|
||||
},
|
||||
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
||||
|
||||
// cache._volumeCache is a map that has a key that includes the volumeId
|
||||
// it is not exactly the volumeId, but it is the key that includes the volumeId
|
||||
// so we can't do cache._volumeCache.get(volumeId) we should iterate
|
||||
// over the keys and find the one that includes the volumeId
|
||||
let volumeCacheKey: string | undefined;
|
||||
|
||||
for (const [key] of cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
dynamicVolume = cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const instance = dynamic4DDisplaySet.instances[0];
|
||||
|
||||
const csv = [];
|
||||
|
||||
// CSV header information with placeholder empty values for the metadata lines
|
||||
csv.push(`Patient ID,${instance.PatientID},`);
|
||||
csv.push(`Study Date,${instance.StudyDate},`);
|
||||
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
|
||||
csv.push(`StudyDescription,${instance.StudyDescription},`);
|
||||
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
|
||||
|
||||
// empty line
|
||||
csv.push('');
|
||||
csv.push('');
|
||||
|
||||
// Helper function to calculate standard deviation
|
||||
function calculateStandardDeviation(data) {
|
||||
const n = data.length;
|
||||
const mean = data.reduce((acc, value) => acc + value, 0) / n;
|
||||
const squaredDifferences = data.map(value => (value - mean) ** 2);
|
||||
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
|
||||
const stdDeviation = Math.sqrt(variance);
|
||||
return stdDeviation;
|
||||
}
|
||||
|
||||
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||
segmentations.forEach((segmentation, segmentationIndex) => {
|
||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
if (summaryStats) {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Operation,Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
|
||||
// // all the voxels in the ROI
|
||||
const mean = [];
|
||||
const min = [];
|
||||
const minIJK = [];
|
||||
const max = [];
|
||||
const maxIJK = [];
|
||||
const std = [];
|
||||
|
||||
const numVoxels = timeData.length;
|
||||
// Helper function to calculate standard deviation
|
||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||
// for each voxel in the ROI, get the value at the current time point
|
||||
const voxelValues = [];
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
||||
}
|
||||
|
||||
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
||||
const minimum = Math.min(...voxelValues);
|
||||
min.push(minimum);
|
||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||
const maximum = Math.max(...voxelValues);
|
||||
max.push(maximum);
|
||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||
std.push(calculateStandardDeviation(voxelValues));
|
||||
}
|
||||
|
||||
let row = `Mean,${segmentation.label}`;
|
||||
// Generate separate rows for each statistic
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${mean[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Standard Deviation,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${std[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Min,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${min[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Max,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${max[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
} else {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// Assuming timeData and ijkCoords are of the same length
|
||||
for (let i = 0; i < timeData.length; i++) {
|
||||
// Generate the pixel identifier
|
||||
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
|
||||
|
||||
// Start a new row for the current pixel
|
||||
let row = `${pixelIdentifier},${segmentation.label}`;
|
||||
|
||||
// Add time data points for this pixel
|
||||
for (let t = 0; t < timeData[i].length; t++) {
|
||||
row += `,${timeData[i][t]}`;
|
||||
}
|
||||
|
||||
// Append the row to the CSV array
|
||||
csv.push(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to CSV string
|
||||
const csvContent = csv.join('\n');
|
||||
|
||||
// Generate filename and trigger download
|
||||
const filename = `${instance.PatientID}.csv`;
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
||||
const computedDisplaySet = displaySet;
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [displaySetCache.keys()];
|
||||
const { displaySetInstanceUID } = computedDisplaySet;
|
||||
// Check to see if computed display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// get the viewports in the grid
|
||||
// iterate over them and find the ones that are showing a dynamic
|
||||
// volume (displaySet), and replace that exact displaySet with the
|
||||
// computed displaySet
|
||||
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
dynamic4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: () => {
|
||||
// Todo: this assumes there is only one dynamic display set in the viewer
|
||||
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
|
||||
const { displaySetInstanceUID } = dynamicDisplaySet;
|
||||
|
||||
// Check to see if dynamic display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// Get the computed 4D display set
|
||||
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
computed4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
|
||||
// get the dynamic 4D display set
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// check if the dynamic 4D display set is in the display, if not we might have
|
||||
// the computed volumes and we should choose them for the segmentation
|
||||
// creation
|
||||
|
||||
let referenceDisplaySet;
|
||||
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
|
||||
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
|
||||
dynamic4DDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (dynamicIsInActiveViewport) {
|
||||
referenceDisplaySet = dynamic4DDisplaySet;
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
// try to see if there is any derived displaySet in the active viewport
|
||||
// which is referencing the dynamic 4D display set
|
||||
|
||||
// Todo: this is wrong but I don't have time to fix it now
|
||||
const cachedDisplaySets = displaySetService.getDisplaySetCache();
|
||||
for (const [key, displaySet] of cachedDisplaySets) {
|
||||
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
|
||||
referenceDisplaySet = displaySet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
throw new Error('No reference display set found based on the dynamic data');
|
||||
}
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
referenceDisplaySet.displaySetInstanceUID,
|
||||
{ label }
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = Array.from(
|
||||
viewports.values(),
|
||||
viewport => viewport.viewportOptions.toolGroupId
|
||||
);
|
||||
|
||||
const representationType = LABELMAP;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
updateSegmentationsChartDisplaySet: {
|
||||
commandFn: actions.updateSegmentationsChartDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
exportTimeReportCSV: {
|
||||
commandFn: actions.exportTimeReportCSV,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: {
|
||||
commandFn: actions.swapDynamicWithComputedDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: {
|
||||
commandFn: actions.createNewLabelMapForDynamicVolume,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: {
|
||||
commandFn: actions.swapComputedWithDynamicDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,654 @@
|
||||
const DEFAULT_COLORMAP = '2hot';
|
||||
const toolGroupIds = {
|
||||
pt: 'dynamic4D-pt',
|
||||
fusion: 'dynamic4D-fusion',
|
||||
ct: 'dynamic4D-ct',
|
||||
};
|
||||
|
||||
function getPTOptions({
|
||||
colormap,
|
||||
voiInverted,
|
||||
}: {
|
||||
colormap?: {
|
||||
name: string;
|
||||
opacity:
|
||||
| number
|
||||
| {
|
||||
value: number;
|
||||
opacity: number;
|
||||
}[];
|
||||
};
|
||||
voiInverted?: boolean;
|
||||
} = {}) {
|
||||
return {
|
||||
blendMode: 'MIP',
|
||||
colormap,
|
||||
voi: {
|
||||
windowWidth: 5,
|
||||
windowCenter: 2.5,
|
||||
},
|
||||
voiInverted,
|
||||
};
|
||||
}
|
||||
|
||||
function getPTViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 1 },
|
||||
{ value: 1, opacity: 1 },
|
||||
],
|
||||
},
|
||||
voiInverted: false,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getFusionViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.3 },
|
||||
{ value: 1, opacity: 0.3 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getSeriesChartViewport() {
|
||||
return {
|
||||
viewportOptions: {
|
||||
viewportId: 'seriesChart',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'chartDisplaySet',
|
||||
options: {
|
||||
// This dataset does not require the download of any instance since it is pre-computed locally,
|
||||
// but interleaveTopToBottom.ts was not loading any series because it consider that all viewports
|
||||
// are a Cornerstone viewport which is not true in this case and it waits for all viewports to
|
||||
// have called interleaveTopToBottom(...).
|
||||
skipLoading: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getCTViewports() {
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const defaultProtocol = {
|
||||
id: 'default4D',
|
||||
locked: true,
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
modifiedDate: '2023-01-01T00:00:00.000Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
attribute: 'ModalitiesInStudy',
|
||||
constraint: {
|
||||
contains: ['CT', 'PT'],
|
||||
},
|
||||
},
|
||||
],
|
||||
// -1 would be used to indicate active only, whereas other values are
|
||||
// the number of required priors referenced - so 0 means active with
|
||||
// 0 or more priors.
|
||||
numberOfPriorsReferenced: -1,
|
||||
displaySetSelectors: {
|
||||
defaultDisplaySetId: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
// Try to match series with images by default, to prevent weird display
|
||||
// on SEG/SR containing studies
|
||||
{
|
||||
attribute: 'numImageFrames',
|
||||
constraint: {
|
||||
greaterThan: { value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ctDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ptDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: 'PT',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: 'Corrected',
|
||||
},
|
||||
},
|
||||
{
|
||||
weight: 2,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
doesNotContain: {
|
||||
value: 'Uncorrected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Should we check if CorrectedImage contains ATTN?
|
||||
// (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
chartDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CHT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'dataPreparation',
|
||||
name: 'Data Preparation',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'registration',
|
||||
name: 'Registration',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'roiQuantification',
|
||||
name: 'ROI Quantification',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'kineticAnalysis',
|
||||
name: 'Kinetic Analysis',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 1 / 2,
|
||||
width: 1,
|
||||
height: 1 / 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), getSeriesChartViewport()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* HangingProtocolModule should provide a list of hanging protocols that will be
|
||||
* available in OHIF for Modes to use to decide on the structure of the viewports
|
||||
* and also the series that hung in the viewports. Each hanging protocol is defined by
|
||||
* { name, protocols}. Examples include the default hanging protocol provided by
|
||||
* the default extension that shows 2x2 viewports.
|
||||
*/
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: defaultProtocol.id,
|
||||
protocol: defaultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getHangingProtocolModule;
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox as NewToolbox } from '@ohif/ui-next';
|
||||
import { Toolbox as OldToolbox } from '@ohif/ui';
|
||||
import { useAppConfig } from '@state';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = ({ renderHeader, getCloseIcon, tab }) => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const Toolbox = appConfig.useExperimentalUI ? NewToolbox : OldToolbox;
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
renderHeader={renderHeader}
|
||||
getCloseIcon={getCloseIcon}
|
||||
tab={tab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicExport = () => {
|
||||
return (
|
||||
<>
|
||||
<DynamicExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dynamic-volume',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-toolbox',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: 'Dynamic Toolbox',
|
||||
component: wrappedDynamicToolbox,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-export',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicExport,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getPanelModule;
|
||||
@@ -0,0 +1,6 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dynamic-volume';
|
||||
|
||||
export { id, SOPClassHandlerName };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { id } from './id';
|
||||
import commandsModule from './commandsModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const dynamicVolumeExtension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
* Perform any pre-registration tasks here. This is called before the extension
|
||||
* is registered. Usually we run tasks such as: configuring the libraries
|
||||
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
|
||||
* this extension is providing.
|
||||
*/
|
||||
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {
|
||||
// TODO: look for the right fix
|
||||
cache.setMaxCacheSize(5 * 1024 * 1024 * 1024);
|
||||
},
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
getPanelModule,
|
||||
/**
|
||||
* ViewportModule should provide a list of viewports that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each viewport is defined by
|
||||
* {name, component} object. Example of a viewport module is the CornerstoneViewport
|
||||
* that is provided by the Cornerstone extension in OHIF.
|
||||
*/
|
||||
getHangingProtocolModule,
|
||||
/**
|
||||
* CommandsModule should provide a list of commands that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each command is defined by
|
||||
* an object of { actions, definitions, defaultContext } where actions is an
|
||||
* object of functions, definitions is an object of available commands, their
|
||||
* options, and defaultContext is the default context for the command to run against.
|
||||
*/
|
||||
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { dynamicVolumeExtension as default };
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
function DynamicDataPanel({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
renderHeader,
|
||||
getCloseIcon,
|
||||
tab,
|
||||
}: withAppTypes) {
|
||||
return (
|
||||
<>
|
||||
{renderHeader && (
|
||||
<>
|
||||
<div className="bg-primary-dark flex select-none rounded-t pt-1.5 pb-[2px]">
|
||||
<div className="flex h-[24px] w-full cursor-pointer select-none justify-center self-center text-[14px]">
|
||||
<div className="text-primary-active flex grow cursor-pointer select-none justify-center self-center text-[13px]">
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getCloseIcon()}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="horizontal"
|
||||
className="bg-black"
|
||||
thickness="2px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicDataPanel;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('dynamicExport');
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export Time Data',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
{
|
||||
label: 'Export ROI Stats',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-3 flex justify-center px-2">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicExport;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
InputDoubleRange,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
IconButton,
|
||||
InputNumber,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
sizeClassName: 'w-[58px] h-[28px]',
|
||||
arrowsDirection: 'horizontal',
|
||||
labelPosition: 'bottom',
|
||||
};
|
||||
|
||||
const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs p-2"
|
||||
>
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="text-primary-active h-[14px] w-[14px]"
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DynamicVolumeControls = ({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
// fps
|
||||
fps,
|
||||
onFpsChange,
|
||||
minFps,
|
||||
maxFps,
|
||||
// Frames
|
||||
currentFrameIndex,
|
||||
onFrameChange,
|
||||
framesLength,
|
||||
onGenerate,
|
||||
onDoubleRangeChange,
|
||||
onDynamicClick,
|
||||
}) => {
|
||||
const [computedView, setComputedView] = useState(false);
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||
|
||||
useEffect(() => {
|
||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||
}, [framesLength]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
|
||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||
return;
|
||||
}
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
title="Controls"
|
||||
childrenClassName="space-y-4 pb-5 px-5"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<ButtonGroup className="mt-2 w-full">
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick?.();
|
||||
}}
|
||||
>
|
||||
4D
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(true);
|
||||
}}
|
||||
>
|
||||
Computed
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FrameControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
framesLength={framesLength}
|
||||
onFrameChange={onFrameChange}
|
||||
currentFrameIndex={currentFrameIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
|
||||
applied to the data set.
|
||||
<br></br> Range Slider: Choose the numeric range within which the operation will be
|
||||
performed.
|
||||
<br></br>Generate Button: Execute the chosen operation on the specified range of
|
||||
data.{' '}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full `}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="w-ful mt-2">
|
||||
<InputDoubleRange
|
||||
values={sliderRangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={0}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
maxValue={framesLength}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||
onClick={() => {
|
||||
onGenerate(computeViewMode);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicVolumeControls;
|
||||
|
||||
function FrameControls({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
fps,
|
||||
minFps,
|
||||
maxFps,
|
||||
onFpsChange,
|
||||
framesLength,
|
||||
onFrameChange,
|
||||
currentFrameIndex,
|
||||
computedView,
|
||||
}) {
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
return (
|
||||
<div className={computedView && 'ohif-disabled'}>
|
||||
<Header
|
||||
title="4D Controls"
|
||||
tooltip={
|
||||
<div>
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
|
||||
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
|
||||
Second) Selector: Adjust the playback speed of the animation.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="mt-3 flex justify-between">
|
||||
<IconButton
|
||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
<InputNumber
|
||||
value={fps}
|
||||
onChange={onFpsChange}
|
||||
minValue={minFps}
|
||||
maxValue={maxFps}
|
||||
{...controlClassNames}
|
||||
label="FPS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { InputDoubleRange } from '@ohif/ui';
|
||||
import { Select } from '@ohif/ui';
|
||||
import { Button } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GenerateVolume = ({
|
||||
rangeValues,
|
||||
handleSliderChange,
|
||||
operationsUI,
|
||||
options,
|
||||
handleGenerateOptionsChange,
|
||||
onGenerateImage,
|
||||
returnTo4D,
|
||||
displayingComputedVolume,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2 text-white">Computed Image</div>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mr-2 bg-black text-white "
|
||||
options={operationsUI}
|
||||
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||
value={options.Operation}
|
||||
onChange={({ value }) => {
|
||||
handleGenerateOptionsChange({
|
||||
Operation: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={rangeValues[0] || 1}
|
||||
maxValue={rangeValues[1] || 2}
|
||||
showLabel={true}
|
||||
step={1}
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={onGenerateImage}
|
||||
className="w-1/2"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={returnTo4D}
|
||||
disabled={!displayingComputedVolume}
|
||||
className="w-1/2"
|
||||
>
|
||||
Return To 4D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateVolume.propTypes = {
|
||||
rangeValues: PropTypes.array.isRequired,
|
||||
handleSliderChange: PropTypes.func.isRequired,
|
||||
operationsUI: PropTypes.array.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||
onGenerateImage: PropTypes.func.isRequired,
|
||||
returnTo4D: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GenerateVolume;
|
||||
@@ -0,0 +1,275 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
|
||||
|
||||
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
|
||||
const { cornerstoneViewportService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const [{ isCineEnabled }, cineService] = useCine();
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||
|
||||
//
|
||||
const uuidComputedVolume = useRef(csUtils.uuidv4());
|
||||
const uuidDynamicVolume = useRef(null);
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
||||
evtDetails.viewportData.data.forEach(volumeData => {
|
||||
if (volumeData.volume.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicVolume = cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
|
||||
|
||||
if (!dynamicVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||
}, [activeViewportId, cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
displaySet,
|
||||
});
|
||||
|
||||
setDisplayingComputed(true);
|
||||
}
|
||||
|
||||
function renderDynamicImage(displaySet) {
|
||||
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
|
||||
}
|
||||
|
||||
// Get computed volume from cache, calculate the data across the time frames,
|
||||
// set the scalar data to the computedVolume, and create displaySet
|
||||
async function onGenerateImage(operationName) {
|
||||
const dynamicVolumeId = dynamicVolume.volumeId;
|
||||
|
||||
if (!dynamicVolumeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||
computedVolume = cache.getVolume(computedVolumeId);
|
||||
}
|
||||
|
||||
const vals = timePointsRangeToUseForGenerate;
|
||||
|
||||
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
||||
|
||||
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
|
||||
dynamicVolume,
|
||||
operationName,
|
||||
operationName === 'SUBTRACT' ? vals : targets
|
||||
);
|
||||
|
||||
// Add loadStatus.loaded to computed volume and set to true
|
||||
computedVolume.loadStatus = {};
|
||||
computedVolume.loadStatus.loaded = true;
|
||||
// Set computed scalar data to volume
|
||||
const scalarData = computedVolume.getScalarData();
|
||||
scalarData.set(dataInTime);
|
||||
|
||||
// If computed display set does not exist, create an object to be used as
|
||||
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||
if (!computedDisplaySet) {
|
||||
const displaySet = {
|
||||
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
|
||||
displaySetInstanceUID: uuidComputedVolume.current,
|
||||
SOPClassHandlerId: SOPClassHandlerId,
|
||||
Modality: dynamicVolume.metadata.Modality,
|
||||
isMultiFrame: false,
|
||||
numImageFrames: 1,
|
||||
uid: uuidComputedVolume.current,
|
||||
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
} else {
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
// Check if viewport is currently displaying the computed volume, if so,
|
||||
// call render on the viewports to update the image, if not, call
|
||||
// renderGeneratedImage
|
||||
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||
// for (const viewportId of viewports.keys()) {
|
||||
// const viewportForRendering =
|
||||
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
// viewportForRendering.render();
|
||||
// }
|
||||
// } else {
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const onPlayPauseChange = isPlaying => {
|
||||
isPlaying ? handlePlay() : handleStop();
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
setIsPlaying(true);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewportInfo;
|
||||
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
setIsPlaying(false);
|
||||
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
cineService.stopClip(element);
|
||||
};
|
||||
|
||||
const handleSetFrameRate = newFrameRate => {
|
||||
setFrameRate(newFrameRate);
|
||||
handleStop();
|
||||
handlePlay();
|
||||
};
|
||||
|
||||
function handleSliderChange(newValues) {
|
||||
if (
|
||||
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimePointsRangeToUseForGenerate(newValues);
|
||||
}
|
||||
|
||||
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicVolumeControls
|
||||
fps={frameRate}
|
||||
isPlaying={isPlaying}
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
minFps={1}
|
||||
maxFps={50}
|
||||
currentFrameIndex={timePointRendered}
|
||||
onFpsChange={handleSetFrameRate}
|
||||
framesLength={timePointsRange[1]}
|
||||
onFrameChange={timePointIndex => {
|
||||
dynamicVolume.timePointIndex = timePointIndex;
|
||||
}}
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||
if (!cache.getVolume(computedVolumeId)) {
|
||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
return computedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
PanelGenerateImage.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
measurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import type { ServicesManager } from '@ohif/core';
|
||||
|
||||
function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }) {
|
||||
const ProgressDropdownWithService =
|
||||
servicesManager.services.customizationService.getCustomization(
|
||||
'progressDropdownWithServiceComponent'
|
||||
).component;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={'workflow-panel'}
|
||||
className="bg-secondary-dark mb-1 px-3 py-4"
|
||||
>
|
||||
<div className="mb-1">Workflow</div>
|
||||
<div>
|
||||
<ProgressDropdownWithService servicesManager={servicesManager} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowPanel;
|
||||
@@ -0,0 +1,5 @@
|
||||
import DynamicDataPanel from './DynamicDataPanel';
|
||||
import WorkflowPanel from './WorkflowPanel';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,6 @@ const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
name: pkg.name,
|
||||
displayName: pkg.name,
|
||||
// rootDir: "../.."
|
||||
// testMatch: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.8.0-beta.53",
|
||||
"version": "3.9.0-beta.97",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -25,6 +25,8 @@
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js",
|
||||
@@ -36,29 +38,31 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.44.1",
|
||||
"@ohif/core": "3.8.0-beta.53",
|
||||
"@ohif/ui": "3.8.0-beta.53",
|
||||
"dcmjs": "^0.29.12",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.82.4",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.9.0-beta.97",
|
||||
"@ohif/ui": "3.9.0-beta.97",
|
||||
"dcmjs": "*",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-resize-detector": "^6.7.6"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-resize-detector": "^10.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.44.1",
|
||||
"@cornerstonejs/core": "^1.44.1",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.44.1",
|
||||
"@cornerstonejs/tools": "^1.44.1",
|
||||
"@kitware/vtk.js": "29.3.01",
|
||||
"@cornerstonejs/adapters": "^1.82.4",
|
||||
"@cornerstonejs/core": "^1.82.4",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
|
||||
"@cornerstonejs/tools": "^1.82.4",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"shader-loader": "^1.3.1",
|
||||
"worker-loader": "^3.0.8"
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import {
|
||||
Enums,
|
||||
eventTarget,
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import { Notification, useViewportDialog } from '@ohif/ui';
|
||||
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
|
||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
|
||||
import './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import CornerstoneServices from '../types/CornerstoneServices';
|
||||
import CinePlayer from '../components/CinePlayer';
|
||||
import { Types } from '@ohif/core';
|
||||
import type { Types } from '@ohif/core';
|
||||
|
||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
import { LutPresentation, PositionPresentation } from '../types/Presentation';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@@ -94,7 +92,7 @@ function areEqual(prevProps, nextProps) {
|
||||
|
||||
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||
// Then we don't need to worry about the re-renders if the props change.
|
||||
const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
|
||||
const {
|
||||
displaySets,
|
||||
dataSource,
|
||||
@@ -103,35 +101,62 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
isJumpToMeasurementDisabled,
|
||||
isJumpToMeasurementDisabled = false,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
// eslint-disable-next-line react/prop-types
|
||||
initialImageIndex,
|
||||
// if the viewport is part of a hanging protocol layout
|
||||
// we should not really rely on the old synchronizers and
|
||||
// you see below we only rehydrate the synchronizers if the viewport
|
||||
// is not part of the hanging protocol layout. HPs should
|
||||
// define their own synchronizers. Since the synchronizers are
|
||||
// viewportId dependent and
|
||||
// eslint-disable-next-line react/prop-types
|
||||
isHangingProtocolLayout,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
if (!viewportId) {
|
||||
throw new Error('Viewport ID is required');
|
||||
}
|
||||
|
||||
// Make sure displaySetOptions has one object per displaySet
|
||||
while (displaySetOptions.length < displaySets.length) {
|
||||
displaySetOptions.push({});
|
||||
}
|
||||
|
||||
// Since we only have support for dynamic data in volume viewports, we should
|
||||
// handle this case here and set the viewportType to volume if any of the
|
||||
// displaySets are dynamic volumes
|
||||
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
|
||||
? 'volume'
|
||||
: viewportOptions.viewportType;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
const elementRef = useRef();
|
||||
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
syncGroupService,
|
||||
cornerstoneViewportService,
|
||||
cornerstoneCacheService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
// useCallback for scroll bar height calculation
|
||||
const setImageScrollBarHeight = useCallback(() => {
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
||||
setScrollbarHeight(scrollbarHeight);
|
||||
}, [elementRef]);
|
||||
|
||||
@@ -151,6 +176,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||
|
||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||
|
||||
viewportActionCornersService.clear(viewportId);
|
||||
},
|
||||
[viewportId]
|
||||
);
|
||||
@@ -177,7 +204,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
const synchronizersStore = stateSyncService.getState().synchronizersStore;
|
||||
|
||||
if (synchronizersStore?.[viewportId]?.length) {
|
||||
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
|
||||
// If the viewport used to have a synchronizer, re apply it again
|
||||
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
|
||||
}
|
||||
@@ -277,7 +304,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
// The presentation state will have been stored previously by closing
|
||||
// a viewport. Otherwise, this viewport will be unchanged and the
|
||||
// presentation information will be directly carried over.
|
||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
||||
const state = stateSyncService.getState();
|
||||
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
|
||||
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
|
||||
|
||||
const { presentationIds } = viewportOptions;
|
||||
const presentations = {
|
||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
@@ -332,45 +362,61 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}
|
||||
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
servicesManager
|
||||
);
|
||||
|
||||
_checkForCachedJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [displaySets, elementRef, viewportId]);
|
||||
|
||||
// Set up the window level action menu in the viewport action corners.
|
||||
useEffect(() => {
|
||||
// Doing an === check here because the default config value when not set is true
|
||||
if (appConfig.addWindowLevelActionMenu === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: In the future we should consider using the customization service
|
||||
// to determine if and in which corner various action components should go.
|
||||
const wlActionMenu = getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
||||
});
|
||||
|
||||
viewportActionCornersService.setComponent({
|
||||
viewportId,
|
||||
id: 'windowLevelActionMenu',
|
||||
component: wlActionMenu,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
indexPriority: -100,
|
||||
});
|
||||
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
|
||||
|
||||
const { ref: resizeRef } = useResizeDetector({
|
||||
onResize,
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<ReactResizeDetector
|
||||
refreshMode="debounce"
|
||||
refreshRate={50} // Wait 50 ms after last move to render
|
||||
onResize={onResize}
|
||||
targetRef={elementRef.current}
|
||||
/>
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
ref={elementRef}
|
||||
ref={el => {
|
||||
resizeRef.current = el;
|
||||
elementRef.current = el;
|
||||
}}
|
||||
></div>
|
||||
<CornerstoneOverlays
|
||||
viewportId={viewportId}
|
||||
@@ -385,7 +431,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute w-full">
|
||||
{/* top offset of 24px to account for ViewportActionCorners. */}
|
||||
<div className="absolute top-[24px] w-full">
|
||||
{viewportDialogState.viewportId === viewportId && (
|
||||
<Notification
|
||||
id="viewport-notification"
|
||||
@@ -394,22 +441,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
actions={viewportDialogState.actions}
|
||||
onSubmit={viewportDialogState.onSubmit}
|
||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||
onKeyPress={viewportDialogState.onKeyPress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
|
||||
<OHIFViewportActionCorners viewportId={viewportId} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}, areEqual);
|
||||
|
||||
function _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
|
||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||
props => {
|
||||
@@ -421,24 +465,17 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
|
||||
// Decide on which viewport should handle this
|
||||
cacheJumpToMeasurementEvent.cornerstoneViewport =
|
||||
cornerstoneViewportService.getViewportIdToJump(
|
||||
jumpId,
|
||||
measurement.displaySetInstanceUID,
|
||||
{ referencedImageId: measurement.referencedImageId }
|
||||
);
|
||||
cornerstoneViewportService.getViewportIdToJump(jumpId, {
|
||||
displaySetInstanceUID: measurement.displaySetInstanceUID,
|
||||
...measurement.metadata,
|
||||
referencedImageId:
|
||||
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||
});
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||
return;
|
||||
}
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -447,13 +484,10 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
|
||||
// Check if there is a queued jumpToMeasurement event
|
||||
function _checkForCachedJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
servicesManager
|
||||
) {
|
||||
if (!cacheJumpToMeasurementEvent) {
|
||||
return;
|
||||
@@ -471,37 +505,15 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
const { measurement } = cacheJumpToMeasurementEvent;
|
||||
if (measurement && elementRef) {
|
||||
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _jumpToMeasurement(
|
||||
measurement,
|
||||
targetElementRef,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesManager) {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
|
||||
const targetElement = targetElementRef.current;
|
||||
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
|
||||
|
||||
if (!SOPInstanceUID) {
|
||||
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||
// to set it properly
|
||||
@@ -513,44 +525,15 @@ function _jumpToMeasurement(
|
||||
|
||||
if (enabledElement) {
|
||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||
const viewport = enabledElement.viewport as IStackViewport | IVolumeViewport;
|
||||
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
|
||||
|
||||
let imageIdIndex = 0;
|
||||
let viewportCameraDirectionMatch = true;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const imageIds = viewport.getImageIds();
|
||||
imageIdIndex = imageIds.findIndex(imageId => {
|
||||
const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } =
|
||||
getSOPInstanceAttributes(imageId);
|
||||
return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber);
|
||||
});
|
||||
} else {
|
||||
// for volume viewport we can't rely on the imageIdIndex since it can be
|
||||
// a reconstructed view that doesn't match the original slice numbers etc.
|
||||
const { viewPlaneNormal: measurementViewPlane } = measurement.metadata;
|
||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
||||
|
||||
// should compare abs for both planes since the direction can be flipped
|
||||
if (
|
||||
measurementViewPlane &&
|
||||
!csUtils.isEqual(measurementViewPlane.map(Math.abs), viewportViewPlane.map(Math.abs))
|
||||
) {
|
||||
viewportCameraDirectionMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewportCameraDirectionMatch || imageIdIndex === -1) {
|
||||
const { metadata } = measurement;
|
||||
if (!viewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })) {
|
||||
console.log("Reference isn't viewable, postponing until updated");
|
||||
return;
|
||||
}
|
||||
|
||||
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||
imageIndex: imageIdIndex,
|
||||
});
|
||||
viewport.setViewReference(metadata);
|
||||
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
// Jump to measurement consumed, remove.
|
||||
@@ -614,10 +597,6 @@ function _rehydrateSynchronizers(
|
||||
// Component displayName
|
||||
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||
|
||||
OHIFCornerstoneViewport.defaultProps = {
|
||||
isJumpToMeasurementDisabled: false,
|
||||
};
|
||||
|
||||
OHIFCornerstoneViewport.propTypes = {
|
||||
displaySets: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.object.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@ import CustomizableViewportOverlay from './CustomizableViewportOverlay';
|
||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||
|
||||
function CornerstoneOverlays(props) {
|
||||
function CornerstoneOverlays(props: withAppTypes) {
|
||||
const { viewportId, element, scrollbarHeight, servicesManager } = props;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [imageSliceData, setImageSliceData] = useState({
|
||||
|
||||
@@ -2,21 +2,24 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { InstanceMetadata } from 'platform/core/src/types';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { InstanceMetadata } from '@ohif/core/src/types';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
import './CustomizableViewportOverlay.css';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
type ViewportData = StackViewportData | VolumeViewportData;
|
||||
|
||||
interface OverlayItemProps {
|
||||
element: any;
|
||||
viewportData: any;
|
||||
element: HTMLElement;
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
servicesManager: ServicesManager;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
viewportId: string;
|
||||
instance: InstanceMetadata;
|
||||
customization: any;
|
||||
formatters: {
|
||||
@@ -35,67 +38,82 @@ interface OverlayItemProps {
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
*/
|
||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const OverlayItemComponents = {
|
||||
'ohif.overlayItem': OverlayItem,
|
||||
'ohif.overlayItem.windowLevel': VOIOverlayItem,
|
||||
'ohif.overlayItem.zoomLevel': ZoomOverlayItem,
|
||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const studyDateItem = {
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
|
||||
};
|
||||
|
||||
const seriesDescriptionItem = {
|
||||
id: 'SeriesDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Series description',
|
||||
condition: ({ referenceInstance }) => {
|
||||
return referenceInstance && referenceInstance.SeriesDescription;
|
||||
},
|
||||
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription
|
||||
};
|
||||
|
||||
const topLeftItems = { id: 'cornerstoneOverlayTopLeft', items: [studyDateItem, seriesDescriptionItem] };
|
||||
|
||||
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
||||
|
||||
const bottomLeftItems = {
|
||||
id: 'cornerstoneOverlayBottomLeft', items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: (props) => {
|
||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
const bottomRightItems = {
|
||||
id: 'cornerstoneOverlayBottomRight',
|
||||
items: [
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Zoom Level Overlay item
|
||||
* The @ohif/cornerstoneOverlay is a default value for a customization
|
||||
* for the cornerstone overlays. The intent is to allow it to be extended
|
||||
* without needing to re-write the individual overlays by using the append
|
||||
* mechanism. Individual attributes can be modified individually without
|
||||
* affecting the other items by using the append as well, with position
|
||||
* based replacement.
|
||||
* This is used as a default in the getCustomizationModule so that it
|
||||
* is available early for additional customization extensions.
|
||||
*/
|
||||
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">Zoom:</span>
|
||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance Number Overlay Item
|
||||
*/
|
||||
function InstanceNumberOverlayItem({
|
||||
instanceNumber,
|
||||
imageSliceData,
|
||||
customization,
|
||||
}: OverlayItemProps) {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">I:</span>
|
||||
<span className="font-light">
|
||||
{instanceNumber !== undefined && instanceNumber !== null
|
||||
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const CornerstoneOverlay = {
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
topLeftItems,
|
||||
topRightItems,
|
||||
bottomLeftItems,
|
||||
bottomRightItems,
|
||||
};
|
||||
|
||||
/**
|
||||
* Customizable Viewport Overlay
|
||||
@@ -106,48 +124,65 @@ function CustomizableViewportOverlay({
|
||||
imageSliceData,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
viewportData: ViewportData;
|
||||
imageSliceData: ImageSliceData;
|
||||
viewportId: string;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
}) {
|
||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
|
||||
servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
const { imageIndex } = imageSliceData;
|
||||
|
||||
const topLeftCustomization = customizationService.getModeCustomization(
|
||||
// The new customization is 'cornerstoneOverlay', with an append or replace
|
||||
// on the individual items rather than defining individual items.
|
||||
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
||||
|
||||
// Historical usage defined the overlays as separate items due to lack of
|
||||
// append functionality. This code enables the historical usage, but
|
||||
// the recommended functionality is to append to the default values in
|
||||
// cornerstoneOverlay rather than defining individual items.
|
||||
const topLeftCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayTopLeft'
|
||||
);
|
||||
const topRightCustomization = customizationService.getModeCustomization(
|
||||
) || cornerstoneOverlay?.topLeftItems;
|
||||
const topRightCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayTopRight'
|
||||
);
|
||||
const bottomLeftCustomization = customizationService.getModeCustomization(
|
||||
) || cornerstoneOverlay?.topRightItems;
|
||||
const bottomLeftCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayBottomLeft'
|
||||
);
|
||||
const bottomRightCustomization = customizationService.getModeCustomization(
|
||||
) || cornerstoneOverlay?.bottomLeftItems;
|
||||
const bottomRightCustomization = customizationService.getCustomization(
|
||||
'cornerstoneOverlayBottomRight'
|
||||
) || cornerstoneOverlay?.bottomRightItems;
|
||||
|
||||
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
viewportData
|
||||
? getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService)
|
||||
: null,
|
||||
[viewportData, viewportId, imageIndex, cornerstoneViewportService]
|
||||
);
|
||||
|
||||
const instance = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getViewportInstance(viewportData, imageIndex);
|
||||
} else {
|
||||
const displaySetProps = useMemo(() => {
|
||||
const displaySets = getDisplaySets(viewportData, displaySetService);
|
||||
if (!displaySets) {
|
||||
return null;
|
||||
}
|
||||
}, [viewportData, imageIndex]);
|
||||
const [displaySet] = displaySets;
|
||||
const { instances, instance: referenceInstance } = displaySet;
|
||||
return {
|
||||
displaySets,
|
||||
displaySet,
|
||||
instance: instances[imageIndex],
|
||||
instances,
|
||||
referenceInstance,
|
||||
};
|
||||
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
|
||||
|
||||
const instanceNumber = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
|
||||
}
|
||||
return null;
|
||||
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
|
||||
|
||||
/**
|
||||
* Initial toolbar state
|
||||
*/
|
||||
useEffect(() => {
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
@@ -190,20 +225,8 @@ function CustomizableViewportOverlay({
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = viewport.getImageData();
|
||||
const scale = viewport.getZoom();
|
||||
|
||||
if (!imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (camera.scale) {
|
||||
setScale(camera.scale);
|
||||
return;
|
||||
}
|
||||
|
||||
const { spacing } = imageData;
|
||||
// convert parallel scale to scale
|
||||
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
setScale(scale);
|
||||
}
|
||||
};
|
||||
@@ -215,26 +238,10 @@ function CustomizableViewportOverlay({
|
||||
};
|
||||
}, [viewportId, viewportData, cornerstoneViewportService, element]);
|
||||
|
||||
/**
|
||||
* Updating the active tools when the toolbar changes
|
||||
*/
|
||||
// Todo: this should act on the toolGroups instead of the toolbar state
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
setActiveTools(toolbarService.getActiveTools());
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const _renderOverlayItem = useCallback(
|
||||
item => {
|
||||
const overlayItemProps: OverlayItemProps = {
|
||||
(item, props) => {
|
||||
const overlayItemProps = {
|
||||
...props,
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
@@ -242,29 +249,27 @@ function CustomizableViewportOverlay({
|
||||
servicesManager,
|
||||
customization: item,
|
||||
formatters: {
|
||||
formatPN: formatPN,
|
||||
formatPN,
|
||||
formatDate: formatDICOMDate,
|
||||
formatTime: formatDICOMTime,
|
||||
formatNumberPrecision: formatNumberPrecision,
|
||||
formatNumberPrecision,
|
||||
},
|
||||
instance,
|
||||
// calculated
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
};
|
||||
|
||||
if (item.customizationType === 'ohif.overlayItem.windowLevel') {
|
||||
return <VOIOverlayItem {...overlayItemProps} />;
|
||||
} else if (item.customizationType === 'ohif.overlayItem.zoomLevel') {
|
||||
return <ZoomOverlayItem {...overlayItemProps} />;
|
||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { customizationType } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[customizationType];
|
||||
|
||||
if (OverlayItemComponent) {
|
||||
return <OverlayItemComponent {...overlayItemProps} />;
|
||||
} else {
|
||||
const renderItem = customizationService.transform(item);
|
||||
|
||||
if (typeof renderItem.content === 'function') {
|
||||
return renderItem.content(overlayItemProps);
|
||||
if (typeof renderItem.contentF === 'function') {
|
||||
return renderItem.contentF(overlayItemProps);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -275,113 +280,90 @@ function CustomizableViewportOverlay({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
customizationService,
|
||||
instance,
|
||||
displaySetProps,
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
]
|
||||
);
|
||||
|
||||
const getTopLeftContent = useCallback(() => {
|
||||
const items = topLeftCustomization?.items || [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`topLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [topLeftCustomization, _renderOverlayItem]);
|
||||
const getContent = useCallback(
|
||||
(customization, keyPrefix) => {
|
||||
if (!customization?.items) {
|
||||
return null;
|
||||
}
|
||||
const { items } = customization;
|
||||
const props = {
|
||||
...displaySetProps,
|
||||
formatters: { formatDate: formatDICOMDate },
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
viewportId,
|
||||
toolGroupService,
|
||||
};
|
||||
|
||||
const getTopRightContent = useCallback(() => {
|
||||
const items = topRightCustomization?.items || [
|
||||
{
|
||||
id: 'InstanceNmber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`topRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [topRightCustomization, _renderOverlayItem]);
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[_renderOverlayItem]
|
||||
);
|
||||
|
||||
const getBottomLeftContent = useCallback(() => {
|
||||
const items = bottomLeftCustomization?.items || [];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [bottomLeftCustomization, _renderOverlayItem]);
|
||||
|
||||
const getBottomRightContent = useCallback(() => {
|
||||
const items = bottomRightCustomization?.items || [];
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [bottomRightCustomization, _renderOverlayItem]);
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={getTopLeftContent()}
|
||||
topRight={getTopRightContent()}
|
||||
bottomLeft={getBottomLeftContent()}
|
||||
bottomRight={getBottomRightContent()}
|
||||
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
|
||||
topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
|
||||
bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
|
||||
bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getViewportInstance(viewportData, imageIndex) {
|
||||
let imageId = null;
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
imageId = viewportData.data.imageIds[imageIndex];
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.data;
|
||||
if (volumes && volumes.length == 1) {
|
||||
const volume = volumes[0];
|
||||
imageId = volume.imageIds[imageIndex];
|
||||
}
|
||||
/**
|
||||
* Gets an array of display sets for the given viewport, based on the viewport data.
|
||||
* Returns null if none found.
|
||||
*/
|
||||
function getDisplaySets(viewportData, displaySetService) {
|
||||
if (!viewportData?.data?.length) {
|
||||
return null;
|
||||
}
|
||||
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
|
||||
if (!displaySets.length) {
|
||||
return null;
|
||||
}
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
|
||||
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
|
||||
let instanceNumber;
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||
|
||||
if (!instanceNumber && instanceNumber !== 0) {
|
||||
return null;
|
||||
}
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
switch (viewportData.viewportType) {
|
||||
case Enums.ViewportType.STACK:
|
||||
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||
break;
|
||||
case Enums.ViewportType.ORTHOGRAPHIC:
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
viewportId,
|
||||
cornerstoneViewportService,
|
||||
imageIndex
|
||||
);
|
||||
break;
|
||||
}
|
||||
return instanceNumber;
|
||||
}
|
||||
|
||||
return instanceNumber ?? null;
|
||||
};
|
||||
|
||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
const imageIds = viewportData.data.imageIds;
|
||||
const imageIds = viewportData.data[0].imageIds;
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
@@ -400,18 +382,24 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
|
||||
|
||||
// Since volume viewports can be in any view direction, they can render
|
||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
|
||||
const volumes = viewportData.volumes;
|
||||
function _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
viewportId,
|
||||
cornerstoneViewportService,
|
||||
imageIndex
|
||||
) {
|
||||
const volumes = viewportData.data;
|
||||
|
||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||
if (!volumes || volumes.length > 1) {
|
||||
if (!volumes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = volumes[0];
|
||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||
const { volume } = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@@ -442,6 +430,90 @@ function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewp
|
||||
}
|
||||
}
|
||||
|
||||
function OverlayItem(props) {
|
||||
const { instance, customization = {} } = props;
|
||||
const { color, attribute, title, label, background } = customization;
|
||||
const value = customization.contentF?.(props, customization) ?? instance?.[attribute];
|
||||
if (value === undefined || value === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color, background }}
|
||||
title={title}
|
||||
>
|
||||
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
|
||||
<span className="ml-1 mr-2 shrink-0">{value}</span>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
*/
|
||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: customization?.color }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom Level Overlay item
|
||||
*/
|
||||
function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">Zoom:</span>
|
||||
<span>{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance Number Overlay Item
|
||||
*/
|
||||
function InstanceNumberOverlayItem({
|
||||
instanceNumber,
|
||||
imageSliceData,
|
||||
customization,
|
||||
}: OverlayItemProps) {
|
||||
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span>
|
||||
{instanceNumber !== undefined && instanceNumber !== null ? (
|
||||
<>
|
||||
<span className="mr-1 shrink-0">I:</span>
|
||||
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
|
||||
</>
|
||||
) : (
|
||||
`${imageIndex + 1}/${numberOfSlices}`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CustomizableViewportOverlay.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
imageIndex: PropTypes.number,
|
||||
@@ -449,3 +521,5 @@ CustomizableViewportOverlay.propTypes = {
|
||||
};
|
||||
|
||||
export default CustomizableViewportOverlay;
|
||||
|
||||
export { CustomizableViewportOverlay, CornerstoneOverlay };
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import { Enums, Types, utilities } from '@cornerstonejs/core';
|
||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||
import { ImageScrollbar } from '@ohif/ui';
|
||||
import { ServicesManger } from '@ohif/core';
|
||||
|
||||
function CornerstoneImageScrollbar({
|
||||
viewportData,
|
||||
@@ -13,8 +12,8 @@ function CornerstoneImageScrollbar({
|
||||
setImageSliceData,
|
||||
scrollbarHeight,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { cineService, cornerstoneViewportService } = (servicesManager as ServicesManger).services;
|
||||
}: withAppTypes) {
|
||||
const { cineService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
@@ -23,7 +22,7 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
if (isCineEnabled) {
|
||||
// on image scrollbar change, stop the CINE if it is playing
|
||||
cineService.stopClip(element);
|
||||
cineService.stopClip(element, { viewportId });
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
}
|
||||
|
||||
@@ -44,67 +43,40 @@ function CornerstoneImageScrollbar({
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const sliceData = utilities.getImageSliceDataForVolumeViewport(
|
||||
viewport as Types.IVolumeViewport
|
||||
);
|
||||
|
||||
if (!sliceData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { imageIndex, numberOfSlices } = sliceData;
|
||||
setImageSliceData({ imageIndex, numberOfSlices });
|
||||
}
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
|
||||
if (!viewportData) {
|
||||
return;
|
||||
}
|
||||
const { viewportType } = viewportData;
|
||||
const eventId =
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
|
||||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
|
||||
Enums.Events.IMAGE_RENDERED;
|
||||
|
||||
const updateStackIndex = event => {
|
||||
const { newImageIdIndex } = event.detail;
|
||||
const updateIndex = event => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
numberOfSlices,
|
||||
});
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
element.addEventListener(eventId, updateIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewportData?.viewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateVolumeIndex = event => {
|
||||
const { imageIndex, numberOfSlices } = event.detail;
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({ imageIndex, numberOfSlices });
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
element.removeEventListener(eventId, updateIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
@@ -113,7 +85,7 @@ function CornerstoneImageScrollbar({
|
||||
onChange={evt => onImageScrollbarChange(evt, viewportId)}
|
||||
max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
|
||||
height={scrollbarHeight}
|
||||
value={imageSliceData.imageIndex}
|
||||
value={imageSliceData.imageIndex || 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Loaded 100 of 1790 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user