Compare commits
670
Commits
v3.8.0-beta.6
...
v3.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d4eabf03 | ||
|
|
5c20bf0faf | ||
|
|
b417063200 | ||
|
|
4bd343ab3e | ||
|
|
a6de628132 | ||
|
|
f4775eccae | ||
|
|
bf8fe7799c | ||
|
|
62b2748847 | ||
|
|
4a125236dd | ||
|
|
a5f03764d1 | ||
|
|
1801a217e3 | ||
|
|
23a2582231 | ||
|
|
1105db5dd1 | ||
|
|
05d41c5206 | ||
|
|
61e3a9cb52 | ||
|
|
4b69279c70 | ||
|
|
8fb8b3bfd1 | ||
|
|
c1a196d802 | ||
|
|
4a564528ba | ||
|
|
aa2b649444 | ||
|
|
3f7ae76113 | ||
|
|
43ad641cf1 | ||
|
|
c80cd6102d | ||
|
|
3d11ef28f2 | ||
|
|
69a81ae838 | ||
|
|
417785ecbc | ||
|
|
7ce7127d7c | ||
|
|
8981a61b8c | ||
|
|
c66c688ace | ||
|
|
d9450104af | ||
|
|
2835a555c8 | ||
|
|
41e8ee692b | ||
|
|
4f20523109 | ||
|
|
0166956dd8 | ||
|
|
1aa2c646a6 | ||
|
|
2e96371b06 | ||
|
|
543c31109b | ||
|
|
2d1704063c | ||
|
|
0610425740 | ||
|
|
ce9ff7d535 | ||
|
|
b4918d8a33 | ||
|
|
a7fd9fa5bf | ||
|
|
1d0259451f | ||
|
|
abf2093d30 | ||
|
|
8fc395e68f | ||
|
|
33af9bb021 | ||
|
|
8dab0883c8 | ||
|
|
a146c9a50e | ||
|
|
2ec4692eaf | ||
|
|
03aad4eba2 | ||
|
|
f14aee2733 | ||
|
|
57eb829173 | ||
|
|
ace67b3bbb | ||
|
|
307b14476b | ||
|
|
433cc8089d | ||
|
|
fa2435d5e9 | ||
|
|
e8f9345b93 | ||
|
|
e254e7bb3d | ||
|
|
7353f7f069 | ||
|
|
3dd0666c0c | ||
|
|
d5bcf54e23 | ||
|
|
ac1cad25af | ||
|
|
e0fca8c88c | ||
|
|
43f54aa49c | ||
|
|
9b2e98a760 | ||
|
|
465df5c573 | ||
|
|
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 | ||
|
|
40b590d75a | ||
|
|
01df1763ce | ||
|
|
5bba98ed84 | ||
|
|
16785936d9 | ||
|
|
3deb73d642 | ||
|
|
ef46b5a704 | ||
|
|
ed2bf23cd2 | ||
|
|
2ff2f3f34d | ||
|
|
a47aeb8bd7 | ||
|
|
94f40192f5 | ||
|
|
1c43aa791a | ||
|
|
e4a116b074 | ||
|
|
2460d28afc | ||
|
|
4a958b8989 | ||
|
|
31b837fa90 | ||
|
|
9bb00d71fa | ||
|
|
40bbe33a9d | ||
|
|
911d672835 | ||
|
|
9c553fded4 | ||
|
|
144dd39393 | ||
|
|
df2f50748e | ||
|
|
83a1523408 | ||
|
|
5a62a983e5 | ||
|
|
d00a86b022 | ||
|
|
710a2dca72 | ||
|
|
9100b1f02e | ||
|
|
0049f4c030 | ||
|
|
b5cdf88a57 | ||
|
|
c793df727b | ||
|
|
1456a493d6 | ||
|
|
7c551f5043 | ||
|
|
0b2c2a235f | ||
|
|
b1efe40aa1 | ||
|
|
a8a0bdb2df | ||
|
|
6878c1a264 | ||
|
|
d181eb4fdc | ||
|
|
fd809e4a0b | ||
|
|
4c29670d76 | ||
|
|
f58725ce40 | ||
|
|
5a1cb842f0 | ||
|
|
c5bd79552a | ||
|
|
61bf22c6f8 | ||
|
|
3d98548072 | ||
|
|
bf9770a55c | ||
|
|
2049c0936c | ||
|
|
7d5607073d | ||
|
|
1b6910bf76 | ||
|
|
98650302c7 | ||
|
|
bf846c94c3 | ||
|
|
b5dc9047ba | ||
|
|
636d8b0458 | ||
|
|
e8858f3eb5 | ||
|
|
eb7c950d7e | ||
|
|
c6214d66b9 | ||
|
|
0eb502aaf0 | ||
|
|
44a5a24d4f | ||
|
|
b0bf5dc780 | ||
|
|
5915fada83 | ||
|
|
2fa335c6d9 | ||
|
|
de4ad88a09 | ||
|
|
6ca13c0a4c | ||
|
|
0177b625ba | ||
|
|
c4064acfbc | ||
|
|
7a05d169ef | ||
|
|
13d0c883f8 | ||
|
|
c45f60dce2 | ||
|
|
7bb2713c5c | ||
|
|
97cb1f94e1 | ||
|
|
f99645f3a1 | ||
|
|
ae16c7f972 | ||
|
|
9c6e1cd305 | ||
|
|
7f0ca857fa | ||
|
|
920256d3a4 | ||
|
|
e1f55e65f2 | ||
|
|
d1c07d90f0 | ||
|
|
b1a04ecac3 | ||
|
|
f707b4ebc9 | ||
|
|
805c53270f | ||
|
|
d14a8f0199 | ||
|
|
5a5c4f58af | ||
|
|
6e5d8b691e | ||
|
|
59576d695d | ||
|
|
78d4bea04d | ||
|
|
806c2f4b36 | ||
|
|
6651008fbb | ||
|
|
74c98b6e9f | ||
|
|
ecee25b1d5 | ||
|
|
f1a67647ae | ||
|
|
4f9a00f872 | ||
|
|
7673a887e4 | ||
|
|
924affa7b5 | ||
|
|
abac6f583e | ||
|
|
36ba20d6e8 | ||
|
|
9d1884d7d8 | ||
|
|
622f18bd3e | ||
|
|
8b6256e76f | ||
|
|
c4ff2c2f09 | ||
|
|
1be5613946 | ||
|
|
6edb1c1e4c | ||
|
|
404b0a5d53 | ||
|
|
786898fd63 | ||
|
|
dfa851600c | ||
|
|
00e751933a | ||
|
|
c4e22c251f | ||
|
|
3cf4b6dea5 | ||
|
|
8c8924af37 | ||
|
|
04121e3713 | ||
|
|
d1eef6dbdb | ||
|
|
d83beb7c62 | ||
|
|
7867b29742 | ||
|
|
4a6f1bd407 | ||
|
|
b694228dd5 | ||
|
|
013068bcb0 | ||
|
|
3fbc23b04c | ||
|
|
323401418e | ||
|
|
fc438e90ae | ||
|
|
9b7f38467b | ||
|
|
fd1251f751 | ||
|
|
14793493a4 | ||
|
|
73d8848707 | ||
|
|
43b1c17209 | ||
|
|
5b513fd30e | ||
|
|
26c9567c38 | ||
|
|
8bbcd0e692 | ||
|
|
8c2f495405 | ||
|
|
e1e1e60b7d | ||
|
|
8af1046803 | ||
|
|
4bbba871f5 | ||
|
|
73e804c311 | ||
|
|
442f99d5eb | ||
|
|
9575a6f54c | ||
|
|
dc345d1875 | ||
|
|
bf252bcec2 | ||
|
|
1a68c06c3b | ||
|
|
a2361fa615 | ||
|
|
ee67bcc0f5 | ||
|
|
f9f16bc69f | ||
|
|
a29f5bc9d3 | ||
|
|
b23eeff937 | ||
|
|
09ff293f5f | ||
|
|
71a30dc9f2 | ||
|
|
330e11c7ff | ||
|
|
c73a403cdc | ||
|
|
5b3b6eed16 | ||
|
|
2a15ef0e44 | ||
|
|
db395852b6 |
No files matched your search
+16
-12
@@ -13,7 +13,7 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@3.1.4
|
||||
cypress: cypress-io/cypress@3.3.1
|
||||
|
||||
executors:
|
||||
cypress-custom:
|
||||
@@ -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
|
||||
@@ -177,10 +180,6 @@ jobs:
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
node ./increaseEventEmitterLimit.mjs
|
||||
- run:
|
||||
name: build half of the packages (to avoid out of memory in circleci)
|
||||
command: |
|
||||
@@ -195,6 +194,7 @@ jobs:
|
||||
###
|
||||
NPM_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- run: yarn -v
|
||||
# Checkout code and ALL Git Tags
|
||||
@@ -226,10 +226,6 @@ jobs:
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
node ./increaseEventEmitterLimit.mjs
|
||||
- run:
|
||||
name: build half of the packages (to avoid out of memory in circleci)
|
||||
command: |
|
||||
@@ -264,6 +260,7 @@ jobs:
|
||||
|
||||
DOCKER_RELEASE_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
@@ -293,6 +290,7 @@ jobs:
|
||||
|
||||
DOCKER_BETA_PUBLISH:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
@@ -379,7 +377,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 +395,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 +494,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,65 @@
|
||||
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/
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* This script uses nodejs to generate a JSON file from a DICOM study folder.
|
||||
* You need to have dcmjs installed in your project.
|
||||
* 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 dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> <optional scheme>
|
||||
*
|
||||
* params:
|
||||
* - 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, scheme = 'dicomweb'] = args;
|
||||
|
||||
if (args.length < 3 || args.length > 4) {
|
||||
console.error(
|
||||
'Usage: node dicom-json-generator.js <studyFolder> <urlPrefix> <outputJSONPath> [scheme]'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const model = {
|
||||
studies: [],
|
||||
};
|
||||
|
||||
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) {
|
||||
try {
|
||||
const files = await recursiveReadDir(studyDirectory);
|
||||
console.debug('Processing...');
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.includes('.DS_Store') && !file.includes('.xml')) {
|
||||
const arrayBuffer = await fs.readFile(file);
|
||||
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
|
||||
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
|
||||
|
||||
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme);
|
||||
processInstance(instance);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Successfully loaded data');
|
||||
|
||||
model.studies.forEach(study => {
|
||||
study.NumInstances = findInstancesNumber(study);
|
||||
study.Modalities = findModalities(study).join('/');
|
||||
});
|
||||
|
||||
await fs.writeFile(outputPath, JSON.stringify(model, null, 2));
|
||||
console.log('JSON saved');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function recursiveReadDir(dir) {
|
||||
let results = [];
|
||||
const list = await fs.readdir(dir);
|
||||
for (const file of list) {
|
||||
const filePath = path.resolve(dir, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
const res = await recursiveReadDir(filePath);
|
||||
results = results.concat(res);
|
||||
} else {
|
||||
results.push(filePath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) {
|
||||
const relativePath = path.relative(studyDirectory, fileLocation);
|
||||
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
|
||||
return `${scheme}:${urlPrefix}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function processInstance(instance) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = instance;
|
||||
let study = getStudy(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
study = createStudyMetadata(StudyInstanceUID, instance);
|
||||
model.studies.push(study);
|
||||
}
|
||||
|
||||
let series = getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
if (!series) {
|
||||
series = createSeriesMetadata(instance);
|
||||
study.series.push(series);
|
||||
}
|
||||
|
||||
const instanceMetaData =
|
||||
instance.NumberOfFrames > 1
|
||||
? createInstanceMetaDataMultiFrame(instance)
|
||||
: createInstanceMetaData(instance);
|
||||
|
||||
series.instances.push(...[].concat(instanceMetaData));
|
||||
}
|
||||
|
||||
function getStudy(StudyInstanceUID) {
|
||||
return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID);
|
||||
}
|
||||
|
||||
function getSeries(StudyInstanceUID, SeriesInstanceUID) {
|
||||
const study = getStudy(StudyInstanceUID);
|
||||
return study
|
||||
? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const findInstancesNumber = study => {
|
||||
let numInstances = 0;
|
||||
study.series.forEach(aSeries => {
|
||||
numInstances = numInstances + aSeries.instances.length;
|
||||
});
|
||||
return numInstances;
|
||||
};
|
||||
|
||||
const findModalities = study => {
|
||||
let modalities = new Set();
|
||||
study.series.forEach(aSeries => {
|
||||
modalities.add(aSeries.Modality);
|
||||
});
|
||||
return Array.from(modalities);
|
||||
};
|
||||
|
||||
function createStudyMetadata(StudyInstanceUID, instance) {
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
StudyDate: instance.StudyDate,
|
||||
StudyTime: instance.StudyTime,
|
||||
PatientName: instance.PatientName,
|
||||
PatientID: instance.PatientID || '1234', // this is critical to have
|
||||
AccessionNumber: instance.AccessionNumber,
|
||||
PatientAge: instance.PatientAge,
|
||||
PatientSex: instance.PatientSex,
|
||||
PatientWeight: instance.PatientWeight,
|
||||
series: [],
|
||||
};
|
||||
}
|
||||
function createSeriesMetadata(instance) {
|
||||
return {
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
Modality: instance.Modality,
|
||||
SliceThickness: instance.SliceThickness,
|
||||
instances: [],
|
||||
};
|
||||
}
|
||||
function commonMetaData(instance) {
|
||||
return {
|
||||
Columns: instance.Columns,
|
||||
Rows: instance.Rows,
|
||||
InstanceNumber: instance.InstanceNumber,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
AcquisitionNumber: instance.AcquisitionNumber,
|
||||
PhotometricInterpretation: instance.PhotometricInterpretation,
|
||||
BitsAllocated: instance.BitsAllocated,
|
||||
BitsStored: instance.BitsStored,
|
||||
PixelRepresentation: instance.PixelRepresentation,
|
||||
SamplesPerPixel: instance.SamplesPerPixel,
|
||||
PixelSpacing: instance.PixelSpacing,
|
||||
HighBit: instance.HighBit,
|
||||
ImageOrientationPatient: instance.ImageOrientationPatient,
|
||||
ImagePositionPatient: instance.ImagePositionPatient,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
ImageType: instance.ImageType,
|
||||
Modality: instance.Modality,
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
WindowCenter: instance.WindowCenter,
|
||||
WindowWidth: instance.WindowWidth,
|
||||
RescaleIntercept: instance.RescaleIntercept,
|
||||
RescaleSlope: instance.RescaleSlope,
|
||||
};
|
||||
}
|
||||
|
||||
function conditionalMetaData(instance) {
|
||||
return {
|
||||
...(instance.ConceptNameCodeSequence && {
|
||||
ConceptNameCodeSequence: instance.ConceptNameCodeSequence,
|
||||
}),
|
||||
...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }),
|
||||
...(instance.ReferencedSeriesSequence && {
|
||||
ReferencedSeriesSequence: instance.ReferencedSeriesSequence,
|
||||
}),
|
||||
...(instance.SharedFunctionalGroupsSequence && {
|
||||
SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence,
|
||||
}),
|
||||
...(instance.PerFrameFunctionalGroupsSequence && {
|
||||
PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence,
|
||||
}),
|
||||
...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }),
|
||||
...(instance.ContentTemplateSequence && {
|
||||
ContentTemplateSequence: instance.ContentTemplateSequence,
|
||||
}),
|
||||
...(instance.CurrentRequestedProcedureEvidenceSequence && {
|
||||
CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence,
|
||||
}),
|
||||
...(instance.CodingSchemeIdentificationSequence && {
|
||||
CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence,
|
||||
}),
|
||||
...(instance.RadiopharmaceuticalInformationSequence && {
|
||||
RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence,
|
||||
}),
|
||||
...(instance.ROIContourSequence && {
|
||||
ROIContourSequence: instance.ROIContourSequence,
|
||||
}),
|
||||
...(instance.StructureSetROISequence && {
|
||||
StructureSetROISequence: instance.StructureSetROISequence,
|
||||
}),
|
||||
...(instance.ReferencedFrameOfReferenceSequence && {
|
||||
ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence,
|
||||
}),
|
||||
...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }),
|
||||
...(instance.Units && { Units: instance.Units }),
|
||||
...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }),
|
||||
...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }),
|
||||
...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }),
|
||||
...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }),
|
||||
...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }),
|
||||
...(instance.FrameTime && { FrameTime: instance.FrameTime }),
|
||||
...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }),
|
||||
...(instance.SequenceOfUltrasoundRegions && {
|
||||
SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createInstanceMetaData(instance) {
|
||||
const metadata = {
|
||||
...commonMetaData(instance),
|
||||
...conditionalMetaData(instance),
|
||||
};
|
||||
return { metadata, url: instance.fileLocation };
|
||||
}
|
||||
|
||||
function createInstanceMetaDataMultiFrame(instance) {
|
||||
const instances = [];
|
||||
const commonData = commonMetaData(instance);
|
||||
const conditionalData = conditionalMetaData(instance);
|
||||
|
||||
for (let i = 1; i <= instance.NumberOfFrames; i++) {
|
||||
const metadata = { ...commonData, ...conditionalData };
|
||||
const result = { metadata, url: instance.fileLocation + `?frame=${i}` };
|
||||
instances.push(result);
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
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
+91
-13
@@ -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": true
|
||||
}
|
||||
"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,20 @@ 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;
|
||||
@@ -96,12 +98,51 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
type: 'filesystem',
|
||||
},
|
||||
module: {
|
||||
noParse: [/(codec)/, /(dicomicc)/],
|
||||
noParse: [/(dicomicc)/],
|
||||
rules: [
|
||||
...(isProdBuild
|
||||
? []
|
||||
: [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: ['react-refresh/babel'],
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: 'source-map-loader',
|
||||
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)$/],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transpileJavaScriptRule(mode),
|
||||
loadWebWorkersRule,
|
||||
@@ -120,6 +161,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: {
|
||||
@@ -133,8 +185,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
'@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',
|
||||
},
|
||||
// Which directories to search when resolving modules
|
||||
modules: [
|
||||
@@ -162,6 +212,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// new BundleAnalyzerPlugin(),
|
||||
],
|
||||
|
||||
+2123
File diff suppressed because it is too large.
Load diff
+148
@@ -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,150 @@ 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/
|
||||
|
||||
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
|
||||
|
||||
From OFFIS DICOM-Team
|
||||
|
||||
https://www.offis.de/
|
||||
OFFIS DICOM-Team
|
||||
@@ -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.
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
# 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](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/devDependencies
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"yarn": ">=1.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"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": "5.5.4",
|
||||
"unused-webpack-plugin": "2.4.0",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "4.7.3",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"workbox-webpack-plugin": "^6.1.5",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "Included as direct dependency"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
# 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](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
|
||||
|
||||
**Note:** Version bump only for package @externals/dicom-microscopy-viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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",
|
||||
"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": "5.5.4",
|
||||
"sharp": "^0.32.6"
|
||||
}
|
||||
}
|
||||
+4
-18
@@ -5,27 +5,11 @@ 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 }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
@@ -45,8 +29,10 @@ module.exports = {
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/transform-destructuring',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
@@ -65,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 @@
|
||||
dd6d9768bbca1d3cc472e8c1e6d85822500b96ef
|
||||
b41706320067b0d9eee181009453fc2a77b4171b
|
||||
@@ -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,394 @@
|
||||
# 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](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.9.0) (2024-11-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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",
|
||||
"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",
|
||||
"@ohif/extension-cornerstone": "3.9.0",
|
||||
"@ohif/extension-default": "3.9.0",
|
||||
"@ohif/i18n": "3.9.0",
|
||||
"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": "^2.2.3",
|
||||
"@cornerstonejs/core": "^2.2.3",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
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 available (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];
|
||||
}
|
||||
|
||||
const getRangeFromPixelData = (pixelData: Float32Array) => {
|
||||
let lowest = pixelData[0];
|
||||
let highest = pixelData[0];
|
||||
|
||||
for (let i = 1; i < pixelData.length; i++) {
|
||||
if (pixelData[i] < lowest) {
|
||||
lowest = pixelData[i];
|
||||
}
|
||||
if (pixelData[i] > highest) {
|
||||
highest = pixelData[i];
|
||||
}
|
||||
}
|
||||
|
||||
return [lowest, highest];
|
||||
};
|
||||
|
||||
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.loading = true;
|
||||
displaySet.isLoaded = false;
|
||||
|
||||
// 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.
|
||||
const promise = _loadParametricMap({
|
||||
extensionManager,
|
||||
displaySet,
|
||||
headers,
|
||||
});
|
||||
|
||||
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = true;
|
||||
// Broadcast that loading is complete
|
||||
servicesManager.services.segmentationService._broadcastEvent(
|
||||
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
{
|
||||
pmapDisplaySet: displaySet,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = false;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _loadParametricMap({ 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,
|
||||
},
|
||||
});
|
||||
|
||||
const newPixelData = new TypedArrayConstructor(pixelData.length);
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
newPixelData[i] = pixelData[i] * 100;
|
||||
}
|
||||
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
|
||||
const range = getRangeFromPixelData(newPixelData);
|
||||
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,205 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const {
|
||||
displaySets,
|
||||
children,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const { displaySetService, segmentationService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
|
||||
// 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 [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
|
||||
|
||||
// Add effect to listen for loading complete
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
|
||||
setPmapIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [pmapDisplaySet]);
|
||||
|
||||
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_2',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.25, opacity: 0.25 },
|
||||
{ value: 0.5, opacity: 0.5 },
|
||||
{ value: 0.75, opacity: 0.75 },
|
||||
{ value: 0.9, opacity: 0.99 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
windowCenter: 50,
|
||||
windowWidth: 100,
|
||||
},
|
||||
});
|
||||
|
||||
uiNotificationService.show({
|
||||
title: 'Parametric Map',
|
||||
type: 'warning',
|
||||
message: 'The values are multiplied by 100 in the viewport for better visibility',
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||
></Component>
|
||||
);
|
||||
}, [
|
||||
extensionManager,
|
||||
displaySetOptions,
|
||||
props,
|
||||
pmapDisplaySet,
|
||||
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]);
|
||||
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (children && children.length) {
|
||||
childrenWithProps = children.map((child, index) => {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{pmapIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="h-full w-full"
|
||||
totalNumbers={null}
|
||||
percentComplete={null}
|
||||
loadingText="Loading Parametric Map..."
|
||||
/>
|
||||
)}
|
||||
{getCornerstoneViewport()}
|
||||
{childrenWithProps}
|
||||
</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.6",
|
||||
"version": "3.9.0",
|
||||
"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.6",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.6",
|
||||
"@ohif/extension-default": "3.8.0-beta.6",
|
||||
"@ohif/i18n": "3.8.0-beta.6",
|
||||
"@ohif/core": "3.9.0",
|
||||
"@ohif/extension-cornerstone": "3.9.0",
|
||||
"@ohif/extension-default": "3.9.0",
|
||||
"@ohif/i18n": "3.9.0",
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const services = servicesManager.services;
|
||||
const { displaySetService, viewportGridService } = services;
|
||||
|
||||
const actions = {
|
||||
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
|
||||
if (displaySet.Modality !== 'RTSTRUCT') {
|
||||
throw new Error('Display set is not an RTSTRUCT');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
// update the previously stored segmentationPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will have the correct segmentation representation hydrated
|
||||
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||
displaySet: displaySet,
|
||||
type: SegmentationRepresentations.Contour,
|
||||
});
|
||||
|
||||
// update the previously stored positionPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
hydrateRTSDisplaySet: {
|
||||
commandFn: actions.hydrateRTSDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'cornerstone-dicom-rt',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
+31
-25
@@ -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 {
|
||||
@@ -68,29 +72,34 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
// Instead of throwing error, subscribe to display sets added
|
||||
const { unsubscribe } = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
({ displaySetsAdded }) => {
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced DisplaySet is missing for the RT');
|
||||
}
|
||||
|
||||
} else {
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
}
|
||||
|
||||
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@@ -107,19 +116,13 @@ function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||
// and also return the same promise to any other callers.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
if (!rtDisplaySet.structureSet) {
|
||||
const structureSet = await loadRTStruct(
|
||||
extensionManager,
|
||||
rtDisplaySet,
|
||||
rtDisplaySet.getReferenceDisplaySet(),
|
||||
headers
|
||||
);
|
||||
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
|
||||
|
||||
rtDisplaySet.structureSet = structureSet;
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
@@ -168,7 +171,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
|
||||
@@ -2,6 +2,7 @@ import { id } from './id';
|
||||
import React from 'react';
|
||||
import { Types } from '@ohif/core';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getCommandsModule from './getCommandsModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
|
||||
@@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
getCommandsModule,
|
||||
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
|
||||
@@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
|
||||
});
|
||||
}
|
||||
|
||||
export default async function loadRTStruct(
|
||||
extensionManager,
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
headers
|
||||
) {
|
||||
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
@@ -102,8 +97,6 @@ export default async function loadRTStruct(
|
||||
const { bulkDataURI } = dataSource.getConfig?.() || {};
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const imageIdSopInstanceUidPairs =
|
||||
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
@@ -149,8 +142,7 @@ export default async function loadRTStruct(
|
||||
|
||||
const contourPoints = [];
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
|
||||
ContourSequenceArray[c];
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
@@ -194,24 +186,6 @@ export default async function loadRTStruct(
|
||||
return structureSet;
|
||||
}
|
||||
|
||||
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||
imageIdSopInstanceUidPairsEntry =>
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
||||
return referencedDisplaySet.images.map(image => {
|
||||
return {
|
||||
imageId: image.imageId,
|
||||
sopInstanceUID: image.SOPInstanceUID,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
|
||||
@@ -10,14 +10,16 @@ function promptHydrateRT({
|
||||
servicesManager,
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
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 => {
|
||||
@@ -27,7 +29,6 @@ function promptHydrateRT({
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
@@ -36,16 +37,18 @@ function promptHydrateRT({
|
||||
});
|
||||
}
|
||||
|
||||
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 +60,7 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
id: 'promptHydrateRT',
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
@@ -66,6 +70,11 @@ function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
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;
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
function OHIFCornerstoneRTViewport(props) {
|
||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
@@ -27,6 +26,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
@@ -64,7 +64,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
@@ -93,12 +96,15 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [rtDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
const hydrateRTDisplaySet = useCallback(
|
||||
({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('hydrateRTSDisplaySet', {
|
||||
displaySet: rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
@@ -113,12 +119,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 +135,6 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
[selectedSegment, segmentationService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -168,6 +176,8 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// I'm not sure what is this, since in RT we support Overlapping segments
|
||||
// via contours
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
@@ -241,7 +251,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
@@ -252,11 +262,27 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [rtDisplaySet]);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a RT and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||
// required if the user has changed the viewport (other viewport than RT viewport)
|
||||
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
@@ -280,68 +306,45 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onStatusClick = async () => {
|
||||
// Before hydrating a RT and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||
// required if the user has changed the viewport (other viewport than RT viewport)
|
||||
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
@@ -363,11 +366,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.6",
|
||||
"version": "3.9.0",
|
||||
"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.6",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.6",
|
||||
"@ohif/extension-default": "3.8.0-beta.6",
|
||||
"@ohif/i18n": "3.8.0-beta.6",
|
||||
"@ohif/core": "3.9.0",
|
||||
"@ohif/extension-cornerstone": "3.9.0",
|
||||
"@ohif/extension-default": "3.9.0",
|
||||
"@ohif/i18n": "3.9.0",
|
||||
"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.20.3",
|
||||
"@cornerstonejs/tools": "^1.20.3",
|
||||
"@kitware/vtk.js": "27.3.1",
|
||||
"@cornerstonejs/adapters": "^2.2.3",
|
||||
"@cornerstonejs/core": "^2.2.3",
|
||||
"@kitware/vtk.js": "32.1.0",
|
||||
"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';
|
||||
@@ -13,17 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
|
||||
import {
|
||||
updateViewportsForSegmentationRendering,
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
} from './utils/hydrationUtils';
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId || activeViewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
return viewport;
|
||||
};
|
||||
|
||||
const {
|
||||
Cornerstone3D: {
|
||||
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
|
||||
Segmentation: { generateSegmentation },
|
||||
},
|
||||
} = adaptersSEG;
|
||||
|
||||
@@ -40,84 +46,14 @@ const commandsModule = ({
|
||||
extensionManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const {
|
||||
uiNotificationService,
|
||||
segmentationService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
toolGroupService,
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
getUpdatedViewportsForSegmentation,
|
||||
/**
|
||||
* Creates an empty segmentation for a specified viewport.
|
||||
* It first checks if the display set associated with the viewport is reconstructable.
|
||||
* If not, it raises a notification error. Otherwise, it creates a new segmentation
|
||||
* for the display set after handling the necessary steps for making the viewport
|
||||
* a volume viewport first
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the target viewport ID.
|
||||
*
|
||||
*/
|
||||
createEmptySegmentationForViewport: async ({ viewportId }) => {
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
// Todo: add support for multiple display sets
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (!displaySet.isReconstructable) {
|
||||
uiNotificationService.show({
|
||||
title: 'Segmentation',
|
||||
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
loadFn: async () => {
|
||||
const currentSegmentations = segmentationService.getSegmentations();
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
{ label: `Segmentation ${currentSegmentations.length + 1}` }
|
||||
);
|
||||
|
||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
||||
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId
|
||||
);
|
||||
|
||||
// Add only one segment for now
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
toolGroupId,
|
||||
segmentIndex: 1,
|
||||
properties: {
|
||||
label: 'Segment 1',
|
||||
},
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads segmentations for a specified viewport.
|
||||
* The function prepares the viewport for rendering, then loads the segmentation details.
|
||||
@@ -129,99 +65,30 @@ const commandsModule = ({
|
||||
*
|
||||
*/
|
||||
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
loadFn: async () => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const segmentation = segmentations[0];
|
||||
const segmentationId = segmentation.id;
|
||||
const label = segmentation.label;
|
||||
const segments = segmentation.segments;
|
||||
|
||||
delete segmentation.segments;
|
||||
|
||||
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
|
||||
segmentationId,
|
||||
label,
|
||||
});
|
||||
|
||||
if (segmentation.scalarData) {
|
||||
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
|
||||
labelmapVolume.scalarData.set(segmentation.scalarData);
|
||||
}
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||
|
||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId
|
||||
);
|
||||
|
||||
segments.forEach(segment => {
|
||||
if (segment === null) {
|
||||
return;
|
||||
}
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: segment.segmentIndex,
|
||||
toolGroupId,
|
||||
properties: {
|
||||
color: segment.color,
|
||||
label: segment.label,
|
||||
opacity: segment.opacity,
|
||||
isLocked: segment.isLocked,
|
||||
visibility: segment.isVisible,
|
||||
active: segmentation.activeSegmentIndex === segment.segmentIndex,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (segmentation.centroidsIJK) {
|
||||
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads segmentation display sets for a specified viewport.
|
||||
* Depending on the modality of the display set (SEG or RTSTRUCT),
|
||||
* it chooses the appropriate service function to create
|
||||
* the segmentation for the display set.
|
||||
* The function then prepares the viewport for rendering segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
|
||||
* @param params.displaySets - Array of display sets to be loaded for segmentation.
|
||||
*
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const displaySet = displaySets[0];
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID: displaySet.referencedDisplaySetInstanceUID,
|
||||
loadFn: async () => {
|
||||
const segDisplaySet = displaySet;
|
||||
const suppressEvents = false;
|
||||
const serviceFunction =
|
||||
segDisplaySet.Modality === 'SEG'
|
||||
? 'createSegmentationForSEGDisplaySet'
|
||||
: 'createSegmentationForRTDisplaySet';
|
||||
const segmentation = segmentations[0];
|
||||
const segmentationId = segmentation.segmentationId;
|
||||
const label = segmentation.config.label;
|
||||
const segments = segmentation.config.segments;
|
||||
|
||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
segmentationId,
|
||||
segments,
|
||||
label,
|
||||
});
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||
|
||||
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
/**
|
||||
* Generates a segmentation from a given segmentation ID.
|
||||
@@ -239,22 +106,61 @@ const commandsModule = ({
|
||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||
|
||||
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
|
||||
const { imageIds } = segmentation.representationData.Labelmap;
|
||||
|
||||
const segmentationVolume = cache.getVolume(segmentationId);
|
||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||||
const referencedImages = referencedVolume.getCornerstoneImages();
|
||||
const segImages = imageIds.map(imageId => cache.getImage(imageId));
|
||||
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
|
||||
|
||||
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
|
||||
const labelmaps2D = [];
|
||||
|
||||
// Generate fake metadata as an example
|
||||
labelmapObj.metadata = [];
|
||||
let z = 0;
|
||||
|
||||
for (const segImage of segImages) {
|
||||
const segmentsOnLabelmap = new Set();
|
||||
const pixelData = segImage.getPixelData();
|
||||
const { rows, columns } = segImage;
|
||||
|
||||
// Use a single pass through the pixel data
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
const segment = pixelData[i];
|
||||
if (segment !== 0) {
|
||||
segmentsOnLabelmap.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
labelmaps2D[z++] = {
|
||||
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
|
||||
pixelData,
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
}
|
||||
|
||||
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
|
||||
|
||||
const labelmap3D = {
|
||||
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
|
||||
metadata: [],
|
||||
labelmaps2D,
|
||||
};
|
||||
|
||||
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
||||
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
|
||||
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
|
||||
|
||||
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
|
||||
// segmentation service already has a color for each segment
|
||||
const segment = segmentationInOHIF?.segments[segmentIndex];
|
||||
const { label, color } = segment;
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { label } = segment;
|
||||
|
||||
const firstRepresentation = representations[0];
|
||||
const color = segmentationService.getSegmentColor(
|
||||
firstRepresentation.viewportId,
|
||||
segmentationId,
|
||||
segment.segmentIndex
|
||||
);
|
||||
|
||||
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
||||
color.slice(0, 3).map(value => value / 255)
|
||||
@@ -263,8 +169,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',
|
||||
@@ -277,12 +183,12 @@ const commandsModule = ({
|
||||
CodeMeaning: 'Tissue',
|
||||
},
|
||||
};
|
||||
labelmapObj.metadata[segmentIndex] = segmentMetadata;
|
||||
labelmap3D.metadata[segmentIndex] = segmentMetadata;
|
||||
});
|
||||
|
||||
const generatedSegmentation = generateSegmentation(
|
||||
referencedImages,
|
||||
labelmapObj,
|
||||
labelmap3D,
|
||||
metaData,
|
||||
options
|
||||
);
|
||||
@@ -324,7 +230,7 @@ const commandsModule = ({
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action !== 1 && promptResult.value) {
|
||||
if (promptResult.action !== 1 && !promptResult.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -397,21 +303,52 @@ 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 = {
|
||||
getUpdatedViewportsForSegmentation: {
|
||||
commandFn: actions.getUpdatedViewportsForSegmentation,
|
||||
},
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: {
|
||||
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
||||
},
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationsForViewport: {
|
||||
commandFn: actions.loadSegmentationsForViewport,
|
||||
},
|
||||
createEmptySegmentationForViewport: {
|
||||
commandFn: actions.createEmptySegmentationForViewport,
|
||||
},
|
||||
|
||||
generateSegmentation: {
|
||||
commandFn: actions.generateSegmentation,
|
||||
},
|
||||
@@ -424,11 +361,18 @@ const commandsModule = ({
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
setBrushSize: {
|
||||
commandFn: actions.setBrushSize,
|
||||
},
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'SEGMENTATION',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
@@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: { allowUnmatchedView: true },
|
||||
viewportOptions: {
|
||||
allowUnmatchedView: true,
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
// options: {
|
||||
// matchingRules: ['sameFOR'],
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'segDisplaySetId',
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import SegmentationToolbox from './panels/SegmentationToolbox';
|
||||
|
||||
const getPanelModule = ({ commandsManager, servicesManager, extensionManager, configuration }) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
return (
|
||||
<>
|
||||
<SegmentationToolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'panelSegmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentation,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationWithTools',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationWithTools,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default getPanelModule;
|
||||
+95
-45
@@ -1,5 +1,6 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
@@ -7,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 {
|
||||
@@ -65,28 +70,32 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
if (!referencedDisplaySet) {
|
||||
// subscribe to display sets added which means at some point it will be available
|
||||
const { unsubscribe } = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
({ displaySetsAdded }) => {
|
||||
// here we can also do a little bit of search, since sometimes DICOM SEG
|
||||
// does not contain the referenced display set uid , and we can just
|
||||
// see which of the display sets added is more similar and assign it
|
||||
// to the referencedDisplaySet
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced DisplaySet is missing for the SEG');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
} else {
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
|
||||
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
|
||||
displaySet.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
}
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||
@@ -94,14 +103,19 @@ 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;
|
||||
|
||||
if (
|
||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||
loadPromises[SOPInstanceUID] &&
|
||||
_segmentationExists(segDisplaySet, segmentationService)
|
||||
_segmentationExists(segDisplaySet)
|
||||
) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
@@ -112,17 +126,21 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
// and also return the same promise to any other callers.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
|
||||
await _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
});
|
||||
try {
|
||||
await _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
});
|
||||
} catch (e) {
|
||||
segDisplaySet.loading = false;
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
segmentationService
|
||||
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
|
||||
.createSegmentationForSEGDisplaySet(segDisplaySet)
|
||||
.then(() => {
|
||||
segDisplaySet.loading = false;
|
||||
resolve();
|
||||
@@ -136,30 +154,35 @@ 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'
|
||||
);
|
||||
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||
|
||||
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
|
||||
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||
segDisplaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
|
||||
);
|
||||
if (!referencedDisplaySet) {
|
||||
throw new Error('referencedDisplaySet is missing for SEG');
|
||||
}
|
||||
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
const { instances: images } = referencedDisplaySet;
|
||||
const imageIds = images.map(({ imageId }) => imageId);
|
||||
|
||||
// Todo: what should be defaults here
|
||||
const tolerance = 0.001;
|
||||
const skipOverlapping = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
@@ -174,18 +197,45 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
|
||||
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
||||
);
|
||||
|
||||
let usedRecommendedDisplayCIELabValue = true;
|
||||
results.segMetadata.data.forEach((data, i) => {
|
||||
if (i > 0) {
|
||||
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
|
||||
data.rgba = data.RecommendedDisplayCIELabValue;
|
||||
|
||||
if (data.rgba) {
|
||||
data.rgba = dicomlabToRGB(data.rgba);
|
||||
} else {
|
||||
usedRecommendedDisplayCIELabValue = false;
|
||||
data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (results.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message:
|
||||
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (!usedRecommendedDisplayCIELabValue) {
|
||||
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
|
||||
uiNotificationService.show({
|
||||
title: 'DICOM SEG import',
|
||||
message:
|
||||
'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.',
|
||||
type: 'warning',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
function _segmentationExists(segDisplaySet) {
|
||||
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
@@ -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.getSegmentationRepresentations(viewportId);
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import React from 'react';
|
||||
|
||||
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,18 +27,9 @@ 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,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
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,295 +0,0 @@
|
||||
import { createReportAsync } from '@ohif/extension-default';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
|
||||
|
||||
import callInputDialog from './callInputDialog';
|
||||
import callColorPickerDialog from './colorPickerDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
}) {
|
||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
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);
|
||||
setSegmentationConfiguration(segmentationService.getConfiguration());
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
return toolGroupIds;
|
||||
};
|
||||
|
||||
const onSegmentationAdd = async () => {
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport');
|
||||
};
|
||||
|
||||
const onSegmentationClick = (segmentationId: string) => {
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
||||
};
|
||||
|
||||
const onSegmentationDelete = (segmentationId: string) => {
|
||||
segmentationService.remove(segmentationId);
|
||||
};
|
||||
|
||||
const onSegmentAdd = segmentationId => {
|
||||
segmentationService.addSegment(segmentationId);
|
||||
};
|
||||
|
||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
// const toolGroupId =
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { label } = segment;
|
||||
|
||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationEdit = segmentationId => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const { label } = segmentation;
|
||||
|
||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
id: segmentationId,
|
||||
label,
|
||||
},
|
||||
false, // suppress event
|
||||
true // notYetUpdatedAtSource
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { color, opacity } = segment;
|
||||
|
||||
const rgbaColor = {
|
||||
r: color[0],
|
||||
g: color[1],
|
||||
b: color[2],
|
||||
a: opacity / 255.0,
|
||||
};
|
||||
|
||||
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
||||
newRgbaColor.r,
|
||||
newRgbaColor.g,
|
||||
newRgbaColor.b,
|
||||
newRgbaColor.a * 255.0,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
const isVisible = !segmentInfo.isVisible;
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
// Todo: right now we apply the visibility to all tool groups
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
isVisible,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
||||
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
const onToggleSegmentationVisibility = segmentationId => {
|
||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
};
|
||||
|
||||
const _setSegmentationConfiguration = useCallback(
|
||||
(segmentationId, key, value) => {
|
||||
segmentationService.setConfiguration({
|
||||
segmentationId,
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
[segmentationService]
|
||||
);
|
||||
|
||||
const onSegmentationDownload = segmentationId => {
|
||||
commandsManager.runCommand('downloadSegmentation', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
const storeSegmentation = async segmentationId => {
|
||||
const datasources = extensionManager.getActiveDataSource();
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand('storeSegmentation', {
|
||||
segmentationId,
|
||||
dataSource: datasources[0],
|
||||
}),
|
||||
reportType: 'Segmentation',
|
||||
});
|
||||
|
||||
// Show the exported report in the active viewport as read only (similar to SR)
|
||||
if (displaySetInstanceUIDs) {
|
||||
// clear the segmentation that we exported, similar to the storeMeasurement
|
||||
// where we remove the measurements and prompt again the user if they would like
|
||||
// to re-read the measurements in a SR read only viewport
|
||||
segmentationService.remove(segmentationId);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSegmentationDownloadRTSS = segmentationId => {
|
||||
commandsManager.runCommand('downloadRTSS', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PanelSegmentation.propTypes = {
|
||||
commandsManager: PropTypes.shape({
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
}),
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
segmentationService: PropTypes.shape({
|
||||
getSegmentation: PropTypes.func.isRequired,
|
||||
getSegmentations: PropTypes.func.isRequired,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
@@ -1,405 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState, useReducer } from 'react';
|
||||
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
|
||||
import { Types } from '@ohif/extension-cornerstone';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const TOOL_TYPES = {
|
||||
CIRCULAR_BRUSH: 'CircularBrush',
|
||||
SPHERE_BRUSH: 'SphereBrush',
|
||||
CIRCULAR_ERASER: 'CircularEraser',
|
||||
SPHERE_ERASER: 'SphereEraser',
|
||||
CIRCLE_SHAPE: 'CircleScissor',
|
||||
RECTANGLE_SHAPE: 'RectangleScissor',
|
||||
SPHERE_SHAPE: 'SphereScissor',
|
||||
THRESHOLD_CIRCULAR_BRUSH: 'ThresholdCircularBrush',
|
||||
THRESHOLD_SPHERE_BRUSH: 'ThresholdSphereBrush',
|
||||
};
|
||||
|
||||
const ACTIONS = {
|
||||
SET_TOOL_CONFIG: 'SET_TOOL_CONFIG',
|
||||
SET_ACTIVE_TOOL: 'SET_ACTIVE_TOOL',
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
Brush: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
|
||||
},
|
||||
Eraser: {
|
||||
brushSize: 15,
|
||||
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
|
||||
},
|
||||
Shapes: {
|
||||
brushSize: 15,
|
||||
mode: 'CircleScissor', // E.g., 'CircleScissor', 'RectangleScissor', or 'SphereScissor'
|
||||
},
|
||||
ThresholdBrush: {
|
||||
brushSize: 15,
|
||||
thresholdRange: [-500, 500],
|
||||
},
|
||||
activeTool: null,
|
||||
};
|
||||
|
||||
function toolboxReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case ACTIONS.SET_TOOL_CONFIG:
|
||||
const { tool, config } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[tool]: {
|
||||
...state[tool],
|
||||
...config,
|
||||
},
|
||||
};
|
||||
case ACTIONS.SET_ACTIVE_TOOL:
|
||||
return { ...state, activeTool: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function SegmentationToolbox({ servicesManager, extensionManager }) {
|
||||
const { toolbarService, segmentationService, toolGroupService } =
|
||||
servicesManager.services as Types.CornerstoneServices;
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const [toolsEnabled, setToolsEnabled] = useState(false);
|
||||
const [state, dispatch] = useReducer(toolboxReducer, initialState);
|
||||
|
||||
const updateActiveTool = useCallback(() => {
|
||||
if (!viewports?.size || activeViewportId === undefined) {
|
||||
return;
|
||||
}
|
||||
const viewport = viewports.get(activeViewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_ACTIVE_TOOL,
|
||||
payload: toolGroupService.getActiveToolForViewport(viewport.viewportId),
|
||||
});
|
||||
}, [activeViewportId, viewports, toolGroupService, dispatch]);
|
||||
|
||||
const setToolActive = useCallback(
|
||||
toolName => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
dispatch({ type: ACTIONS.SET_ACTIVE_TOOL, payload: toolName });
|
||||
},
|
||||
[toolbarService, dispatch]
|
||||
);
|
||||
|
||||
/**
|
||||
* sets the tools enabled IF there are segmentations
|
||||
*/
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
segmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
segmentationService.EVENTS.SEGMENTATION_UPDATED,
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
];
|
||||
|
||||
const unsubscriptions = [];
|
||||
|
||||
events.forEach(event => {
|
||||
const { unsubscribe } = segmentationService.subscribe(event, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
const activeSegmentation = segmentations?.find(seg => seg.isActive);
|
||||
|
||||
setToolsEnabled(activeSegmentation?.segmentCount > 0);
|
||||
});
|
||||
|
||||
unsubscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
updateActiveTool();
|
||||
|
||||
return () => {
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
};
|
||||
}, [activeViewportId, viewports, segmentationService, updateActiveTool]);
|
||||
|
||||
/**
|
||||
* Update the active tool when the toolbar state changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
updateActiveTool();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService, updateActiveTool]);
|
||||
|
||||
useEffect(() => {
|
||||
// if the active tool is not a brush tool then do nothing
|
||||
if (!Object.values(TOOL_TYPES).includes(state.activeTool)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is enabled then do nothing
|
||||
if (toolsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the tool is Segmentation and it is disabled, then switch
|
||||
// back to the window level tool to not confuse the user when no
|
||||
// segmentation is active or when there is no segment in the segmentation
|
||||
setToolActive('WindowLevel');
|
||||
}, [toolsEnabled, state.activeTool, setToolActive]);
|
||||
|
||||
const updateBrushSize = useCallback(
|
||||
(toolName, brushSize) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
},
|
||||
[toolGroupService]
|
||||
);
|
||||
|
||||
const onBrushSizeChange = useCallback(
|
||||
(valueAsStringOrNumber, toolCategory) => {
|
||||
const value = Number(valueAsStringOrNumber);
|
||||
|
||||
_getToolNamesFromCategory(toolCategory).forEach(toolName => {
|
||||
updateBrushSize(toolName, value);
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: toolCategory,
|
||||
config: { brushSize: value },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch]
|
||||
);
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
newRange => {
|
||||
if (
|
||||
newRange[0] === state.ThresholdBrush.thresholdRange[0] &&
|
||||
newRange[1] === state.ThresholdBrush.thresholdRange[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolNames = _getToolNamesFromCategory('ThresholdBrush');
|
||||
|
||||
toolNames.forEach(toolName => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD_INSIDE_CIRCLE: {
|
||||
threshold: newRange,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_TOOL_CONFIG,
|
||||
payload: {
|
||||
tool: 'ThresholdBrush',
|
||||
config: { thresholdRange: newRange },
|
||||
},
|
||||
});
|
||||
},
|
||||
[toolGroupService, dispatch, state.ThresholdBrush.thresholdRange]
|
||||
);
|
||||
|
||||
return (
|
||||
<AdvancedToolbox
|
||||
title="Segmentation Tools"
|
||||
items={[
|
||||
{
|
||||
name: 'Brush',
|
||||
icon: 'icon-tool-brush',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'brush-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Brush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Brush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'brush-mode',
|
||||
value: state.Brush.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Eraser',
|
||||
icon: 'icon-tool-eraser',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCULAR_ERASER ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_ERASER,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCULAR_ERASER),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
type: 'range',
|
||||
id: 'eraser-radius',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.Eraser.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'Eraser'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'eraser-mode',
|
||||
value: state.Eraser.mode,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCULAR_ERASER, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.SPHERE_ERASER, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Shapes',
|
||||
icon: 'icon-tool-shape',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.CIRCLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.RECTANGLE_SHAPE ||
|
||||
state.activeTool === TOOL_TYPES.SPHERE_SHAPE,
|
||||
onClick: () => setToolActive(TOOL_TYPES.CIRCLE_SHAPE),
|
||||
options: [
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
value: state.Shapes.mode,
|
||||
id: 'shape-mode',
|
||||
values: [
|
||||
{ value: TOOL_TYPES.CIRCLE_SHAPE, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.RECTANGLE_SHAPE, label: 'Rectangle' },
|
||||
{ value: TOOL_TYPES.SPHERE_SHAPE, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Threshold Tool',
|
||||
icon: 'icon-tool-threshold',
|
||||
disabled: !toolsEnabled,
|
||||
active:
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH ||
|
||||
state.activeTool === TOOL_TYPES.THRESHOLD_SPHERE_BRUSH,
|
||||
onClick: () => setToolActive(TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH),
|
||||
options: [
|
||||
{
|
||||
name: 'Radius (mm)',
|
||||
id: 'threshold-radius',
|
||||
type: 'range',
|
||||
min: 0.5,
|
||||
max: 99.5,
|
||||
value: state.ThresholdBrush.brushSize,
|
||||
step: 0.5,
|
||||
onChange: value => onBrushSizeChange(value, 'ThresholdBrush'),
|
||||
},
|
||||
{
|
||||
name: 'Mode',
|
||||
type: 'radio',
|
||||
id: 'threshold-mode',
|
||||
value: state.activeTool,
|
||||
values: [
|
||||
{ value: TOOL_TYPES.THRESHOLD_CIRCULAR_BRUSH, label: 'Circle' },
|
||||
{ value: TOOL_TYPES.THRESHOLD_SPHERE_BRUSH, label: 'Sphere' },
|
||||
],
|
||||
onChange: value => setToolActive(value),
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
id: 'segmentation-threshold-range',
|
||||
children: () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-secondary-light h-[1px]"></div>
|
||||
<div className="mt-1 text-[13px] text-white">Threshold</div>
|
||||
<InputDoubleRange
|
||||
values={state.ThresholdBrush.thresholdRange}
|
||||
onChange={handleRangeChange}
|
||||
minValue={-1000}
|
||||
maxValue={1000}
|
||||
step={1}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
showAdjustmentArrows={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getToolNamesFromCategory(category) {
|
||||
let toolNames = [];
|
||||
switch (category) {
|
||||
case 'Brush':
|
||||
toolNames = ['CircularBrush', 'SphereBrush'];
|
||||
break;
|
||||
case 'Eraser':
|
||||
toolNames = ['CircularEraser', 'SphereEraser'];
|
||||
break;
|
||||
case 'ThresholdBrush':
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return toolNames;
|
||||
}
|
||||
|
||||
export default SegmentationToolbox;
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
|
||||
function callInputDialog(uiDialogService, label, callback) {
|
||||
const dialogId = 'enter-segment-label';
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.label, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment',
|
||||
value: { label },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
return (
|
||||
<Input
|
||||
label="Enter the segment label"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
autoFocus
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
}}
|
||||
onKeyPress={event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default callInputDialog;
|
||||
@@ -1,33 +0,0 @@
|
||||
// Todo: use defaults in cs3d
|
||||
const initialState = {
|
||||
renderOutline: true,
|
||||
renderFill: true,
|
||||
outlineOpacity: 0.9,
|
||||
outlineWidth: 3,
|
||||
fillOpacity: 0.9,
|
||||
fillOpacityInactive: 0.8,
|
||||
renderInactiveSegmentations: true,
|
||||
};
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'RENDER_OUTLINE':
|
||||
return { ...state, renderOutline: action.payload.value };
|
||||
case 'RENDER_FILL':
|
||||
return { ...state, renderFill: action.payload.value };
|
||||
case 'SET_OUTLINE_OPACITY':
|
||||
return { ...state, outlineOpacity: action.payload.value };
|
||||
case 'SET_OUTLINE_WIDTH':
|
||||
return { ...state, outlineWidth: action.payload.value };
|
||||
case 'SET_FILL_OPACITY':
|
||||
return { ...state, fillOpacity: action.payload.value };
|
||||
case 'SET_FILL_OPACITY_INACTIVE':
|
||||
return { ...state, fillOpacityInactive: action.payload.value };
|
||||
case 'RENDER_INACTIVE_SEGMENTATIONS':
|
||||
return { ...state, renderInactiveSegmentations: action.payload.value };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState, reducer };
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SegmentationPanelMode {
|
||||
Expanded = 'expanded',
|
||||
Dropdown = 'dropdown',
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import { Enums, cache } from '@cornerstonejs/core';
|
||||
|
||||
/**
|
||||
* Updates the viewports in preparation for rendering segmentations.
|
||||
* Evaluates each viewport to determine which need modifications,
|
||||
* then for those viewports, changes them to a volume type and ensures
|
||||
* they are ready for segmentation rendering.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @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.
|
||||
*
|
||||
* @returns Returns true upon successful update of viewports for segmentation rendering.
|
||||
*/
|
||||
async function updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
loadFn,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
}: {
|
||||
viewportId: string;
|
||||
loadFn: () => Promise<string>;
|
||||
servicesManager: any;
|
||||
referencedDisplaySetInstanceUID?: string;
|
||||
}) {
|
||||
const { cornerstoneViewportService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
|
||||
const updatedViewports = getUpdatedViewportsForSegmentation({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
referencedDisplaySetInstanceUID,
|
||||
});
|
||||
|
||||
// create Segmentation callback which needs to be waited until
|
||||
// the volume is created (if coming from stack)
|
||||
const createSegmentationForVolume = async () => {
|
||||
const segmentationId = await loadFn();
|
||||
segmentationService.hydrateSegmentation(segmentationId);
|
||||
};
|
||||
|
||||
// the reference volume that is used to draw the segmentation. so check if the
|
||||
// volume exists in the cache (the target Viewport is already a volume viewport)
|
||||
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
|
||||
volumeId.includes(referencedDisplaySetInstanceUID)
|
||||
);
|
||||
|
||||
updatedViewports.forEach(async viewport => {
|
||||
viewport.viewportOptions = {
|
||||
...viewport.viewportOptions,
|
||||
viewportType: 'volume',
|
||||
needsRerendering: true,
|
||||
};
|
||||
const viewportId = viewport.viewportId;
|
||||
|
||||
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) {
|
||||
await createSegmentationForVolume();
|
||||
return;
|
||||
}
|
||||
|
||||
const createNewSegmentationWhenVolumeMounts = async evt => {
|
||||
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
|
||||
ac.uid.includes(referencedDisplaySetInstanceUID)
|
||||
);
|
||||
|
||||
// Note: make sure to re-grab the viewport since it might have changed
|
||||
// during the time it took for the volume to be mounted, for instance
|
||||
// the stack viewport has been changed to a volume viewport
|
||||
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
volumeViewport.setCamera(prevCamera);
|
||||
|
||||
volumeViewport.element.removeEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
createNewSegmentationWhenVolumeMounts
|
||||
);
|
||||
|
||||
if (!isTheActiveViewportVolumeMounted) {
|
||||
// it means it is one of those other updated viewports so just update the camera
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportId === targetViewportId) {
|
||||
await createSegmentationForVolume();
|
||||
}
|
||||
};
|
||||
|
||||
csViewport.element.addEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
createNewSegmentationWhenVolumeMounts
|
||||
);
|
||||
});
|
||||
|
||||
// Set the displaySets for the viewports that require to be updated
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId || activeViewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
return viewport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
referencedDisplaySetInstanceUID,
|
||||
}) {
|
||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
|
||||
|
||||
const referenceDisplaySetInstanceUID =
|
||||
referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
|
||||
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
targetViewportId,
|
||||
referenceDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
viewports.forEach((viewport, viewportId) => {
|
||||
if (
|
||||
targetViewportId === viewportId ||
|
||||
updatedViewports.find(v => v.viewportId === viewportId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewport.displaySetInstanceUIDs,
|
||||
segmentationFrameOfReferenceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
needsRerendering: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return updatedViewports;
|
||||
}
|
||||
|
||||
export {
|
||||
updateViewportsForSegmentationRendering,
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
};
|
||||
@@ -11,24 +11,30 @@ function promptHydrateSEG({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateSEGDisplaySet,
|
||||
}) {
|
||||
hydrateCallback,
|
||||
}: 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 hydrateCallback({
|
||||
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';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
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}`;
|
||||
@@ -40,7 +37,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
}
|
||||
|
||||
const segDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
@@ -64,7 +60,11 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet,
|
||||
segDisplaySet
|
||||
@@ -101,29 +101,29 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, segDisplaySet]}
|
||||
displaySets={[segDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
viewportType: viewportOptions.viewportType,
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementDisabled={onElementDisabled}
|
||||
// initialImageIndex={initialImageIndex}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -141,12 +141,35 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
);
|
||||
|
||||
const hydrateSEG = useCallback(() => {
|
||||
// update the previously stored segmentationPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will have the correct segmentation representation hydrated
|
||||
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||
displaySet: segDisplaySet,
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
});
|
||||
|
||||
// update the previously stored positionPresentation with the new viewportId
|
||||
// presentation so that when we put the referencedDisplaySet back in the viewport
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segIsLoading) {
|
||||
return;
|
||||
@@ -157,29 +180,24 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateSEGDisplaySet,
|
||||
hydrateCallback: hydrateSEG,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
|
||||
|
||||
useEffect(() => {
|
||||
// on new seg display set, remove all segmentations from all viewports
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
|
||||
if (evt.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message: 'Overlapping segments detected which is not currently supported',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -233,28 +251,78 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep the already stored segmentationPresentation for this viewport in memory
|
||||
// so that we can restore it after hydrating the SEG
|
||||
commandsManager.runCommand('updateStoredSegmentationPresentation', {
|
||||
displaySet: segDisplaySet,
|
||||
type: SegmentationRepresentations.Labelmap,
|
||||
});
|
||||
|
||||
// always start fresh for this viewport since it is special type of viewport
|
||||
// that should only show one segmentation at a time.
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
// This creates a custom tool group which has the lifetime of this view
|
||||
// only, and does NOT interfere with currently displayed segmentations.
|
||||
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
// e.g., another seg displayset is dragged into the viewport
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
// Only destroy the viewport specific implementation
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(segDisplaySet.isHydrated);
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
hydrateSEG();
|
||||
}, [storePresentationState, hydrateSEG]);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
@@ -279,73 +347,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
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
|
||||
@@ -362,18 +365,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSEGViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
OHIFCornerstoneSEGViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
||||
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
|
||||
|
||||
|
||||
@@ -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.6",
|
||||
"version": "3.9.0",
|
||||
"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.6",
|
||||
"@ohif/extension-cornerstone": "3.8.0-beta.6",
|
||||
"@ohif/extension-measurement-tracking": "3.8.0-beta.6",
|
||||
"@ohif/ui": "3.8.0-beta.6",
|
||||
"dcmjs": "^0.29.11",
|
||||
"@ohif/core": "3.9.0",
|
||||
"@ohif/extension-cornerstone": "3.9.0",
|
||||
"@ohif/extension-measurement-tracking": "3.9.0",
|
||||
"@ohif/ui": "3.9.0",
|
||||
"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.20.3",
|
||||
"@cornerstonejs/core": "^1.20.3",
|
||||
"@cornerstonejs/tools": "^1.20.3",
|
||||
"@cornerstonejs/adapters": "^2.2.3",
|
||||
"@cornerstonejs/core": "^2.2.3",
|
||||
"@cornerstonejs/tools": "^2.2.3",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -40,7 +40,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
||||
return dataset;
|
||||
};
|
||||
|
||||
const commandsModule = ({}) => {
|
||||
const commandsModule = (props: withAppTypes) => {
|
||||
const { servicesManager } = props;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const actions = {
|
||||
/**
|
||||
*
|
||||
@@ -55,7 +57,7 @@ const commandsModule = ({}) => {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -95,7 +97,15 @@ const commandsModule = ({}) => {
|
||||
throw new Error('Invalid report, no content');
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
const onBeforeDicomStore =
|
||||
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
|
||||
|
||||
let dicomDict;
|
||||
if (typeof onBeforeDicomStore === 'function') {
|
||||
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
|
||||
|
||||
if (StudyInstanceUID) {
|
||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
@@ -118,13 +128,9 @@ const commandsModule = ({}) => {
|
||||
const definitions = {
|
||||
downloadReport: {
|
||||
commandFn: actions.downloadReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
storeMeasurements: {
|
||||
commandFn: actions.storeMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 '../enums';
|
||||
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 };
|
||||
+114
-101
@@ -1,26 +1,25 @@
|
||||
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) {
|
||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
const {
|
||||
commandsManager,
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportLabel,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
@@ -28,8 +27,12 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { displaySetService, cornerstoneViewportService, measurementService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
measurementService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
@@ -48,6 +51,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;
|
||||
@@ -65,6 +70,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
trackedMeasurements = tracked?.[0];
|
||||
sendTrackedMeasurementsEvent = tracked?.[1];
|
||||
}
|
||||
|
||||
if (!sendTrackedMeasurementsEvent) {
|
||||
// if no panels from measurement-tracking extension is used, this code will run
|
||||
trackedMeasurements = null;
|
||||
@@ -74,6 +80,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||
if (displaySets.length) {
|
||||
viewportGridService.setDisplaySetsForViewports([
|
||||
@@ -86,6 +93,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
/**
|
||||
* Store the tracking identifiers per viewport in order to be able to
|
||||
* show the SR measurements on the referenced image on the correct viewport,
|
||||
@@ -128,11 +140,19 @@ 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,
|
||||
displaySetService
|
||||
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
|
||||
if (!referencedDisplaySet || !referencedDisplaySetMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMeasurementSelected(newMeasurementSelected);
|
||||
setActiveImageDisplaySetData(referencedDisplaySet);
|
||||
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
|
||||
@@ -149,6 +169,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// new measurement
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!csViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = csViewport.getImageIds();
|
||||
|
||||
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
|
||||
@@ -202,7 +226,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 +240,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 +283,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 +299,61 @@ 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;
|
||||
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.addComponents([
|
||||
{
|
||||
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 +374,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 +384,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 +404,17 @@ 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);
|
||||
}
|
||||
|
||||
if (!displaySetInstanceUID) {
|
||||
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
@@ -429,6 +442,7 @@ function _getStatusComponent({
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}) {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
@@ -437,7 +451,6 @@ function _getStatusComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
@@ -516,4 +529,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;
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
POINT: 'POINT',
|
||||
MULTIPOINT: 'MULTIPOINT',
|
||||
POLYLINE: 'POLYLINE',
|
||||
CIRCLE: 'CIRCLE',
|
||||
ELLIPSE: 'ELLIPSE',
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
export const SCOORDTypes = {
|
||||
POINT: 'POINT',
|
||||
MULTIPOINT: 'MULTIPOINT',
|
||||
POLYLINE: 'POLYLINE',
|
||||
CIRCLE: 'CIRCLE',
|
||||
ELLIPSE: 'ELLIPSE',
|
||||
};
|
||||
|
||||
export const CodeNameCodeSequenceValues = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
ImagingMeasurements: '126010',
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
FindingSiteSCT: '363698007', // SCT
|
||||
};
|
||||
|
||||
export const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
SCT: 'SCT',
|
||||
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
|
||||
};
|
||||
|
||||
export const RelationshipType = {
|
||||
INFERRED_FROM: 'INFERRED FROM',
|
||||
CONTAINS: 'CONTAINS',
|
||||
};
|
||||
|
||||
const enums = {
|
||||
CodeNameCodeSequenceValues,
|
||||
CodingSchemeDesignators,
|
||||
RelationshipType,
|
||||
SCOORDTypes,
|
||||
};
|
||||
|
||||
export default enums;
|
||||
@@ -1,30 +1,37 @@
|
||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
import addSRAnnotation from './utils/addSRAnnotation';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import {
|
||||
SOPClassHandlerName,
|
||||
SOPClassHandlerId,
|
||||
SOPClassHandlerId3D,
|
||||
SOPClassHandlerName3D,
|
||||
} from './id';
|
||||
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
|
||||
|
||||
const { sopClassDictionary } = utils;
|
||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
type InstanceMetadata = Types.InstanceMetadata;
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||
|
||||
// TODO ->
|
||||
// Add SR thumbnail
|
||||
// Make viewport
|
||||
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
|
||||
/**
|
||||
* TODO
|
||||
* - [ ] Add SR thumbnail
|
||||
* - [ ] Make viewport
|
||||
* - [ ] 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:
|
||||
sopClassDictionary.BasicTextSR,
|
||||
sopClassDictionary.EnhancedSR,
|
||||
sopClassDictionary.ComprehensiveSR,
|
||||
];
|
||||
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
|
||||
const validateSameStudyUID = (uid: string, instances): void => {
|
||||
instances.forEach(it => {
|
||||
if (it.StudyInstanceUID !== uid) {
|
||||
@@ -34,31 +41,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'],
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
INFERRED_FROM: 'INFERRED FROM',
|
||||
CONTAINS: 'CONTAINS',
|
||||
};
|
||||
|
||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||
|
||||
/**
|
||||
* Adds instances to the DICOM SR series, rather than creating a new
|
||||
* series, so that as SR's are saved, they append to the series, and the
|
||||
@@ -85,7 +67,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 +95,12 @@ 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 is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR;
|
||||
|
||||
const isImagingMeasurementReport =
|
||||
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
|
||||
|
||||
const displaySet = {
|
||||
//plugin: id,
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
@@ -133,13 +109,14 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
instances,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
isImagingMeasurementReport,
|
||||
sopClassUids,
|
||||
instance,
|
||||
addInstances,
|
||||
@@ -150,115 +127,206 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(displaySet, servicesManager, extensionManager) {
|
||||
/**
|
||||
* Loads the display set with the given services and extension manager.
|
||||
* @param srDisplaySet - The display set to load.
|
||||
* @param servicesManager - The services manager containing displaySetService and measurementService.
|
||||
* @param extensionManager - The extension manager containing data sources.
|
||||
*/
|
||||
async function _load(
|
||||
srDisplaySet: Types.DisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager: AppTypes.ExtensionManager
|
||||
) {
|
||||
const { displaySetService, measurementService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const { ContentSequence } = srDisplaySet.instance;
|
||||
|
||||
const { ContentSequence } = displaySet.instance;
|
||||
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: srDisplaySet.instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID,
|
||||
});
|
||||
if (parentObj && key) {
|
||||
parentObj[key] = new Float32Array(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
if (srDisplaySet.isLoaded !== true) {
|
||||
await retrieveBulkData(ContentSequence);
|
||||
}
|
||||
|
||||
if (srDisplaySet.isImagingMeasurementReport) {
|
||||
srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
srDisplaySet.measurements = _getMeasurements(ContentSequence);
|
||||
} else {
|
||||
srDisplaySet.referencedImages = [];
|
||||
srDisplaySet.measurements = [];
|
||||
}
|
||||
|
||||
const mappings = measurementService.getSourceMappings(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||
);
|
||||
|
||||
displaySet.isHydrated = false;
|
||||
displaySet.isRehydratable = isRehydratable(displaySet, mappings);
|
||||
displaySet.isLoaded = true;
|
||||
srDisplaySet.isHydrated = false;
|
||||
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
|
||||
srDisplaySet.isLoaded = true;
|
||||
|
||||
// Check currently added displaySets and add measurements if the sources exist.
|
||||
/** Check currently added displaySets and add measurements if the sources exist */
|
||||
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
activeDisplaySet,
|
||||
dataSource,
|
||||
servicesManager
|
||||
);
|
||||
});
|
||||
|
||||
// Subscribe to new displaySets as the source may come in after.
|
||||
/** Subscribe to new displaySets as the source may come in after */
|
||||
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
|
||||
const { displaySetsAdded } = data;
|
||||
// If there are still some measurements that have not yet been loaded into cornerstone,
|
||||
// See if we can load them onto any of the new displaySets.
|
||||
/**
|
||||
* If there are still some measurements that have not yet been loaded into cornerstone,
|
||||
* See if we can load them onto any of the new displaySets.
|
||||
*/
|
||||
displaySetsAdded.forEach(newDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(displaySet, newDisplaySet, dataSource);
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource,
|
||||
servicesManager
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
|
||||
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
/**
|
||||
* Checks if measurements can be added to a display set.
|
||||
*
|
||||
* @param srDisplaySet - The source display set containing measurements.
|
||||
* @param newDisplaySet - The new display set to check if measurements can be added.
|
||||
* @param dataSource - The data source used to retrieve image IDs.
|
||||
* @param servicesManager - The services manager.
|
||||
*/
|
||||
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource,
|
||||
servicesManager: AppTypes.ServicesManager
|
||||
) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
measurement => measurement.loaded === false
|
||||
);
|
||||
|
||||
if (unloadedMeasurements.length === 0) {
|
||||
// All already loaded!
|
||||
if (
|
||||
unloadedMeasurements.length === 0 ||
|
||||
!(newDisplaySet instanceof ImageSet) ||
|
||||
newDisplaySet.unsupported
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newDisplaySet instanceof ImageSet) {
|
||||
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||
// const { sopClassUids } = newDisplaySet;
|
||||
// Create a Set for faster lookups
|
||||
// const sopClassUidSet = new Set(sopClassUids);
|
||||
|
||||
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
|
||||
const imageIdMap = new Map<string, string>();
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
||||
|
||||
for (const imageId of imageIds) {
|
||||
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
|
||||
const key = `${SOPInstanceUID}:${frameNumber || 1}`;
|
||||
imageIdMap.set(key, imageId);
|
||||
}
|
||||
|
||||
if (!unloadedMeasurements?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sopClassUids, images } = newDisplaySet;
|
||||
const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR;
|
||||
|
||||
// Check if any have the newDisplaySet is the correct SOPClass.
|
||||
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
|
||||
measurement.coords.some(coord =>
|
||||
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
|
||||
)
|
||||
);
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
let measurement = unloadedMeasurements[j];
|
||||
|
||||
if (unloadedMeasurements.length === 0) {
|
||||
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
|
||||
return;
|
||||
}
|
||||
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
|
||||
'onBeforeSRAddMeasurement'
|
||||
)?.value;
|
||||
|
||||
const SOPInstanceUIDs = [];
|
||||
|
||||
unloadedMeasurements.forEach(measurement => {
|
||||
const { coords } = measurement;
|
||||
|
||||
coords.forEach(coord => {
|
||||
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
|
||||
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||
SOPInstanceUIDs.push(SOPInstanceUID);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
||||
|
||||
for (const imageId of imageIdsForDisplaySet) {
|
||||
if (!unloadedMeasurements.length) {
|
||||
// All measurements loaded.
|
||||
return;
|
||||
if (typeof onBeforeSRAddMeasurement === 'function') {
|
||||
measurement = onBeforeSRAddMeasurement({
|
||||
measurement,
|
||||
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
|
||||
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
|
||||
// if it is 3d SR we can just add the SR annotation
|
||||
if (is3DSR) {
|
||||
addSRAnnotation(measurement, null, null);
|
||||
measurement.loaded = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
const measurement = unloadedMeasurements[j];
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
|
||||
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
|
||||
const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence;
|
||||
if (!referencedSOPSequence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
}
|
||||
const { ReferencedSOPInstanceUID } = referencedSOPSequence;
|
||||
const frame = referencedSOPSequence.ReferencedFrameNumber || 1;
|
||||
const key = `${ReferencedSOPInstanceUID}:${frame}`;
|
||||
const imageId = imageIdMap.get(key);
|
||||
|
||||
if (
|
||||
imageId &&
|
||||
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
|
||||
) {
|
||||
addSRAnnotation(measurement, imageId, frame);
|
||||
|
||||
// Update measurement properties
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frame;
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a measurement references a specific SOP Instance UID.
|
||||
* @param measurement - The measurement object.
|
||||
* @param SOPInstanceUID - The SOP Instance UID to check against.
|
||||
* @param frameNumber - The frame number to check against (optional).
|
||||
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
|
||||
*/
|
||||
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
|
||||
const { coords } = measurement;
|
||||
|
||||
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
|
||||
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||
/**
|
||||
* NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
|
||||
* 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)) {
|
||||
@@ -268,40 +336,62 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
||||
for (let j = 0; j < coords.length; j++) {
|
||||
const coord = coords[j];
|
||||
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
|
||||
|
||||
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the SOP class handler module.
|
||||
*
|
||||
* @param {Object} options - The options for retrieving the SOP class handler module.
|
||||
* @param {Object} options.servicesManager - The services manager.
|
||||
* @param {Object} options.extensionManager - The extension manager.
|
||||
* @returns {Array} An array containing the SOP class handler module.
|
||||
*/
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: SOPClassHandlerName,
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
{
|
||||
name: SOPClassHandlerName3D,
|
||||
sopClassUids: [sopClassDictionary.Comprehensive3DSR],
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
|
||||
*
|
||||
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
|
||||
* @returns {Array} - The array of measurements.
|
||||
*/
|
||||
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
|
||||
);
|
||||
|
||||
if (!ImagingMeasurements) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
|
||||
);
|
||||
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers =
|
||||
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
|
||||
|
||||
const measurements = [];
|
||||
|
||||
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
||||
@@ -310,7 +400,6 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
|
||||
|
||||
const measurement = _processMeasurement(mergedContentSequence);
|
||||
|
||||
if (measurement) {
|
||||
measurements.push(measurement);
|
||||
}
|
||||
@@ -320,6 +409,12 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
return measurements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves merged content sequences by tracking unique identifiers.
|
||||
*
|
||||
* @param {Array} MeasurementGroups - The measurement groups.
|
||||
* @returns {Object} - The merged content sequences by tracking unique identifiers.
|
||||
*/
|
||||
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
||||
|
||||
@@ -331,7 +426,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||
);
|
||||
|
||||
if (!TrackingUniqueIdentifierItem) {
|
||||
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
|
||||
}
|
||||
@@ -360,6 +454,15 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
|
||||
return mergedContentSequencesByTrackingUniqueIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the measurement based on the merged content sequence.
|
||||
* If the merged content sequence contains SCOORD or SCOORD3D value types,
|
||||
* it calls the _processTID1410Measurement function.
|
||||
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
|
||||
*
|
||||
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
|
||||
* @returns {any} - The processed measurement result.
|
||||
*/
|
||||
function _processMeasurement(mergedContentSequence) {
|
||||
if (
|
||||
mergedContentSequence.some(
|
||||
@@ -372,11 +475,21 @@ function _processMeasurement(mergedContentSequence) {
|
||||
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes TID 1410 style measurements from the mergedContentSequence.
|
||||
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
|
||||
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
|
||||
*
|
||||
* @param mergedContentSequence - The merged content sequence containing the measurements.
|
||||
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
|
||||
*/
|
||||
function _processTID1410Measurement(mergedContentSequence) {
|
||||
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
||||
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
||||
|
||||
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
|
||||
const graphicItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||
);
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
||||
|
||||
@@ -403,7 +516,6 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
||||
@@ -411,12 +523,29 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
}
|
||||
});
|
||||
|
||||
const findingSites = mergedContentSequence.filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT &&
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
|
||||
);
|
||||
if (findingSites.length) {
|
||||
measurement.labels.push({
|
||||
label: CodeNameCodeSequenceValues.FindingSiteSCT,
|
||||
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
|
||||
return measurement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the non-geometrically defined measurement from the merged content sequence.
|
||||
*
|
||||
* @param mergedContentSequence The merged content sequence containing the measurement data.
|
||||
* @returns The processed measurement object.
|
||||
*/
|
||||
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
||||
|
||||
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||
@@ -446,10 +575,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
||||
) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
|
||||
value: finding.ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
@@ -461,12 +590,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
||||
);
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
|
||||
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
@@ -476,15 +606,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
|
||||
|
||||
const { ValueType } = ContentSequence;
|
||||
|
||||
if (!ValueType === 'SCOORD') {
|
||||
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
||||
|
||||
if (coords) {
|
||||
measurement.coords.push(coords);
|
||||
}
|
||||
@@ -499,59 +626,62 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
function _getCoordsFromSCOORDOrSCOORD3D(item) {
|
||||
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
|
||||
|
||||
if (
|
||||
!(
|
||||
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
|
||||
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
|
||||
)
|
||||
) {
|
||||
console.warn(
|
||||
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
|
||||
* @param {object} graphicItem - The graphic item containing the coordinates.
|
||||
* @returns {object} - The extracted coordinates.
|
||||
*/
|
||||
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
|
||||
const { ValueType, GraphicType, GraphicData } = graphicItem;
|
||||
const coords = { ValueType, GraphicType, GraphicData };
|
||||
|
||||
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
|
||||
if (ValueType === 'SCOORD') {
|
||||
const { ReferencedSOPSequence } = item.ContentSequence;
|
||||
|
||||
coords.ReferencedSOPSequence = ReferencedSOPSequence;
|
||||
} else if (ValueType === 'SCOORD3D') {
|
||||
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
|
||||
|
||||
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
|
||||
}
|
||||
|
||||
coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence;
|
||||
coords.ReferencedFrameOfReferenceSequence =
|
||||
graphicItem.ReferencedFrameOfReferenceUID ||
|
||||
graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence;
|
||||
return coords;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
|
||||
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
|
||||
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
|
||||
* @returns {Object} - An object containing the label and value.
|
||||
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
|
||||
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
|
||||
* Example: { label: 'Long Axis', value: '31.00 mm' }
|
||||
*/
|
||||
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
|
||||
const { CodeMeaning } = ConceptNameCodeSequence;
|
||||
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
||||
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||
|
||||
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
|
||||
|
||||
return {
|
||||
label: CodeMeaning,
|
||||
value: `${formatedNumericValue} ${CodeValue}`,
|
||||
}; // E.g. Long Axis: 31.0 mm
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
|
||||
*
|
||||
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
|
||||
* @returns {Array} - The list of referenced images.
|
||||
*/
|
||||
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
|
||||
);
|
||||
|
||||
if (!ImageLibrary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||
);
|
||||
if (!ImageLibraryGroup) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const referencedImages = [];
|
||||
|
||||
@@ -575,6 +705,15 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
return referencedImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a DICOM sequence to an array.
|
||||
* If the sequence is null or undefined, an empty array is returned.
|
||||
* If the sequence is already an array, it is returned as is.
|
||||
* Otherwise, the sequence is wrapped in an array and returned.
|
||||
*
|
||||
* @param {any} sequence - The DICOM sequence to convert.
|
||||
* @returns {any[]} - The converted array.
|
||||
*/
|
||||
function _getSequenceAsArray(sequence) {
|
||||
if (!sequence) {
|
||||
return [];
|
||||
|
||||
@@ -5,4 +5,7 @@ const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dicom-sr';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
|
||||
export { SOPClassHandlerName, SOPClassHandlerId, id };
|
||||
const SOPClassHandlerName3D = 'dicom-sr-3d';
|
||||
const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`;
|
||||
|
||||
export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id };
|
||||
@@ -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';
|
||||
@@ -8,9 +8,10 @@ import { id } from './id.js';
|
||||
import toolNames from './tools/toolNames';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||
import Enums from './enums';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
|
||||
return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSRViewport = props => {
|
||||
@@ -70,4 +71,4 @@ const dicomSRExtension = {
|
||||
export default dicomSRExtension;
|
||||
|
||||
// Put static exports here so they can be type checked
|
||||
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
|
||||
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames };
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
addTool,
|
||||
AngleTool,
|
||||
annotation,
|
||||
ArrowAnnotateTool,
|
||||
@@ -9,34 +8,64 @@ import {
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
RectangleROITool,
|
||||
utilities as csToolsUtils,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { Types, MeasurementService } from '@ohif/core';
|
||||
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
||||
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
|
||||
import addToolInstance from './utils/addToolInstance';
|
||||
import { Types } from '@ohif/core';
|
||||
import toolNames from './tools/toolNames';
|
||||
|
||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||
|
||||
/**
|
||||
* @param {object} configuration
|
||||
*/
|
||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||
addTool(DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
export default function init({
|
||||
configuration = {},
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams): void {
|
||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
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);
|
||||
addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool);
|
||||
|
||||
// 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);
|
||||
|
||||
const csTools3DVer1MeasurementSource = measurementService.getSource(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||
);
|
||||
|
||||
const { POINT } = measurementService.VALUE_TYPES;
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'SRSCOORD3DPoint',
|
||||
POINT,
|
||||
SRSCOOR3DProbeMapper.toAnnotation,
|
||||
SRSCOOR3DProbeMapper.toMeasurement
|
||||
);
|
||||
|
||||
// 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 +74,31 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
SRPlanarFreehandROI: dashedLine,
|
||||
SRRectangleROI: dashedLine,
|
||||
global: {},
|
||||
});
|
||||
|
||||
measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
|
||||
({ viewportId, measurement, isConsumed }) => {
|
||||
if (isConsumed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { viewPlaneNormal } = currentViewport.getCamera();
|
||||
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
|
||||
currentViewport as StackViewport,
|
||||
measurement.points[0],
|
||||
viewPlaneNormal
|
||||
);
|
||||
const imageIndex = (currentViewport as StackViewport)
|
||||
.getImageIds()
|
||||
.indexOf(referencedImageId);
|
||||
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
||||
} catch (error) {
|
||||
console.warn('Unable to jump to image based on measurement coordinate', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { SOPClassHandlerId, SOPClassHandlerId3D } from './id';
|
||||
|
||||
export default function onModeEnter({ servicesManager }) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
|
||||
const srDisplaySets = [...displaySetCache.values()].filter(
|
||||
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
||||
ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D
|
||||
);
|
||||
|
||||
srDisplaySets.forEach(ds => {
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
Types as cs3DToolsTypes,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
|
||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||
import { SCOORDTypes } from '../enums';
|
||||
import toolNames from './toolNames';
|
||||
|
||||
export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
static toolName = 'DICOMSRDisplay';
|
||||
static toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
constructor(
|
||||
toolProps = {},
|
||||
@@ -22,14 +23,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 +66,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 +78,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 +103,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
color,
|
||||
lineDash,
|
||||
lineWidth,
|
||||
...groupStyle,
|
||||
};
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
@@ -107,19 +113,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
let canvasCoordinatesAdapter;
|
||||
|
||||
switch (GraphicType) {
|
||||
case SCOORD_TYPES.POINT:
|
||||
case SCOORDTypes.POINT:
|
||||
renderMethod = this.renderPoint;
|
||||
break;
|
||||
case SCOORD_TYPES.MULTIPOINT:
|
||||
case SCOORDTypes.MULTIPOINT:
|
||||
renderMethod = this.renderMultipoint;
|
||||
break;
|
||||
case SCOORD_TYPES.POLYLINE:
|
||||
case SCOORDTypes.POLYLINE:
|
||||
renderMethod = this.renderPolyLine;
|
||||
break;
|
||||
case SCOORD_TYPES.CIRCLE:
|
||||
case SCOORDTypes.CIRCLE:
|
||||
renderMethod = this.renderEllipse;
|
||||
break;
|
||||
case SCOORD_TYPES.ELLIPSE:
|
||||
case SCOORDTypes.ELLIPSE:
|
||||
renderMethod = this.renderEllipse;
|
||||
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
|
||||
break;
|
||||
@@ -160,6 +166,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
const drawingOptions = {
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
};
|
||||
let allCanvasCoordinates = [];
|
||||
renderableData.map((data, index) => {
|
||||
@@ -223,7 +230,11 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
// This gives us one point for arrow
|
||||
canvasCoordinates.push(viewport.worldToCanvas(point));
|
||||
|
||||
// We get the other point for the arrow by using the image size
|
||||
if (data[1] !== undefined) {
|
||||
canvasCoordinates.push(viewport.worldToCanvas(data[1]));
|
||||
}
|
||||
else{
|
||||
// We get the other point for the arrow by using the image size
|
||||
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
|
||||
|
||||
let xOffset = 10;
|
||||
@@ -242,6 +253,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
]);
|
||||
|
||||
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
|
||||
|
||||
}
|
||||
|
||||
|
||||
const arrowUID = `${index}`;
|
||||
|
||||
@@ -307,6 +321,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
{
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -328,7 +343,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
}
|
||||
|
||||
const { annotationUID, data = {} } = annotation;
|
||||
const { label } = data;
|
||||
const { labels } = data;
|
||||
const { color } = options;
|
||||
|
||||
let adaptedCanvasCoordinates = canvasCoordinates;
|
||||
@@ -336,10 +351,12 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
if (typeof canvasCoordinatesAdapter === 'function') {
|
||||
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
|
||||
}
|
||||
const textLines = this._getTextBoxLinesFromLabels(label);
|
||||
const textLines = this._getTextBoxLinesFromLabels(labels);
|
||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
|
||||
|
||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
|
||||
if (!annotation.data?.handles?.textBox?.worldPosition) {
|
||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
|
||||
}
|
||||
|
||||
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import {
|
||||
annotation,
|
||||
drawing,
|
||||
utilities,
|
||||
Types as cs3DToolsTypes,
|
||||
AnnotationDisplayTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import toolNames from './toolNames';
|
||||
import { Annotation } from '@cornerstonejs/tools/dist/types/types';
|
||||
|
||||
export default class SCOORD3DPointTool extends AnnotationDisplayTool {
|
||||
static toolName = toolNames.SRSCOORD3DPoint;
|
||||
|
||||
constructor(
|
||||
toolProps = {},
|
||||
defaultToolProps = {
|
||||
configuration: {},
|
||||
}
|
||||
) {
|
||||
super(toolProps, defaultToolProps);
|
||||
}
|
||||
|
||||
_getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 5);
|
||||
const lines = [];
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// This tool should not inherit from AnnotationTool and we should not need
|
||||
// to add the following lines.
|
||||
isPointNearTool = () => null;
|
||||
getHandleNearImagePoint = () => null;
|
||||
|
||||
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
|
||||
const { viewport } = enabledElement;
|
||||
const { element } = viewport;
|
||||
|
||||
const annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
||||
|
||||
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
|
||||
if (!annotations?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredAnnotations = annotations;
|
||||
if (!viewport._actors?.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
|
||||
toolGroupId: this.toolGroupId,
|
||||
toolName: this.getToolName(),
|
||||
viewportId: enabledElement.viewport.id,
|
||||
};
|
||||
|
||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||
const annotation = filteredAnnotations[i];
|
||||
|
||||
const annotationUID = annotation.annotationUID;
|
||||
const { renderableData } = annotation.data;
|
||||
const { POINT: points } = renderableData;
|
||||
|
||||
styleSpecifier.annotationUID = annotationUID;
|
||||
|
||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||
const color = this.getStyle('color', styleSpecifier, annotation);
|
||||
|
||||
const options = {
|
||||
color,
|
||||
lineDash,
|
||||
lineWidth,
|
||||
};
|
||||
|
||||
const point = points[0][0];
|
||||
|
||||
// check if viewport can render it
|
||||
const viewable = viewport.isReferenceViewable(
|
||||
{ FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point },
|
||||
{ asNearbyProjection: true }
|
||||
);
|
||||
|
||||
if (!viewable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// render the point
|
||||
const arrowPointCanvas = viewport.worldToCanvas(point);
|
||||
// Todo: configure this
|
||||
const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20];
|
||||
const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas];
|
||||
|
||||
drawing.drawArrow(
|
||||
svgDrawingHelper,
|
||||
annotationUID,
|
||||
'1',
|
||||
canvasCoordinates[1],
|
||||
canvasCoordinates[0],
|
||||
{
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
}
|
||||
);
|
||||
|
||||
this.renderTextBox(
|
||||
svgDrawingHelper,
|
||||
viewport,
|
||||
canvasCoordinates,
|
||||
annotation,
|
||||
styleSpecifier,
|
||||
options
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderTextBox(
|
||||
svgDrawingHelper,
|
||||
viewport,
|
||||
canvasCoordinates,
|
||||
annotation,
|
||||
styleSpecifier,
|
||||
options = {}
|
||||
) {
|
||||
if (!canvasCoordinates || !annotation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { annotationUID, data = {} } = annotation;
|
||||
const { labels } = data;
|
||||
|
||||
const textLines = [];
|
||||
|
||||
for (const label of labels) {
|
||||
// make this generic
|
||||
// fix this
|
||||
if (label.label === '363698007') {
|
||||
textLines.push(`Finding Site: ${label.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { color } = options;
|
||||
|
||||
const adaptedCanvasCoordinates = canvasCoordinates;
|
||||
// adapt coordinates if there is an adapter
|
||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
|
||||
|
||||
if (!annotation.data?.handles?.textBox?.worldPosition) {
|
||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
|
||||
}
|
||||
|
||||
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
|
||||
|
||||
const textBoxUID = '1';
|
||||
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
|
||||
|
||||
const boundingBox = drawing.drawLinkedTextBox(
|
||||
svgDrawingHelper,
|
||||
annotationUID,
|
||||
textBoxUID,
|
||||
textLines,
|
||||
textBoxPosition,
|
||||
canvasCoordinates,
|
||||
{},
|
||||
{
|
||||
...textBoxOptions,
|
||||
color,
|
||||
}
|
||||
);
|
||||
|
||||
const { x: left, y: top, width, height } = boundingBox;
|
||||
|
||||
annotation.data.handles.textBox.worldBoundingBox = {
|
||||
topLeft: viewport.canvasToWorld([left, top]),
|
||||
topRight: viewport.canvasToWorld([left + width, top]),
|
||||
bottomLeft: viewport.canvasToWorld([left, top + height]),
|
||||
bottomRight: viewport.canvasToWorld([left + width, top + height]),
|
||||
};
|
||||
}
|
||||
|
||||
public getLinkedTextBoxStyle(
|
||||
specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier,
|
||||
annotation?: Annotation
|
||||
): Record<string, unknown> {
|
||||
// Todo: this function can be used to set different styles for different toolMode
|
||||
// for the textBox.
|
||||
|
||||
return {
|
||||
visibility: this.getStyle('textBoxVisibility', specifications, annotation),
|
||||
fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation),
|
||||
fontSize: this.getStyle('textBoxFontSize', specifications, annotation),
|
||||
color: this.getStyle('textBoxColor', specifications, annotation),
|
||||
shadow: this.getStyle('textBoxShadow', specifications, annotation),
|
||||
background: this.getStyle('textBoxBackground', specifications, annotation),
|
||||
lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation),
|
||||
lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
|
||||
|
||||
const toolNames = {
|
||||
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
|
||||
DICOMSRDisplay: 'DICOMSRDisplay',
|
||||
SRLength: 'SRLength',
|
||||
SRBidirectional: 'SRBidirectional',
|
||||
SREllipticalROI: 'SREllipticalROI',
|
||||
@@ -11,6 +9,7 @@ const toolNames = {
|
||||
SRCobbAngle: 'SRCobbAngle',
|
||||
SRRectangleROI: 'SRRectangleROI',
|
||||
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
|
||||
SRSCOORD3DPoint: 'SRSCOORD3DPoint',
|
||||
};
|
||||
|
||||
export default toolNames;
|
||||
@@ -0,0 +1,62 @@
|
||||
const SRSCOOR3DProbe = {
|
||||
toAnnotation: measurement => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType,
|
||||
customizationService
|
||||
) => {
|
||||
const { annotation } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Probe tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName } = metadata;
|
||||
const { points } = data.handles;
|
||||
|
||||
const displayText = getDisplayText(annotation);
|
||||
return {
|
||||
uid: annotationUID,
|
||||
points,
|
||||
metadata,
|
||||
toolName: metadata.toolName,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType?.(toolName) ?? null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getDisplayText(annotation) {
|
||||
const { data } = annotation;
|
||||
|
||||
if (!data) {
|
||||
return [''];
|
||||
}
|
||||
const { labels } = data;
|
||||
|
||||
const displayText = [];
|
||||
|
||||
for (const label of labels) {
|
||||
// make this generic
|
||||
if (label.label === '33636980076') {
|
||||
displayText.push(`Finding Site: ${label.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
export default SRSCOOR3DProbe;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Types, annotation } from '@cornerstonejs/tools';
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
|
||||
import getRenderableData from './getRenderableData';
|
||||
import toolNames from '../tools/toolNames';
|
||||
|
||||
export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
||||
let toolName = toolNames.DICOMSRDisplay;
|
||||
const renderableData = measurement.coords.reduce((acc, coordProps) => {
|
||||
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
|
||||
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const { TrackingUniqueIdentifier } = measurement;
|
||||
const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0];
|
||||
const graphicTypePoints = renderableData[graphicType];
|
||||
|
||||
/** TODO: Read the tool name from the DICOM SR identification type in the future. */
|
||||
let frameOfReferenceUID = null;
|
||||
|
||||
if (imageId) {
|
||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||
frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID;
|
||||
}
|
||||
|
||||
if (valueType === 'SCOORD3D') {
|
||||
toolName = toolNames.SRSCOORD3DPoint;
|
||||
|
||||
// get the ReferencedFrameOfReferenceUID from the measurement
|
||||
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
|
||||
}
|
||||
|
||||
const SRAnnotation: Types.Annotation = {
|
||||
annotationUID: TrackingUniqueIdentifier,
|
||||
highlighted: false,
|
||||
isLocked: false,
|
||||
invalidated: false,
|
||||
metadata: {
|
||||
toolName,
|
||||
valueType,
|
||||
graphicType,
|
||||
FrameOfReferenceUID: frameOfReferenceUID,
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
label: measurement.labels?.[0]?.value || undefined,
|
||||
displayText: measurement.displayText || undefined,
|
||||
handles: {
|
||||
textBox: measurement.textBox ?? {},
|
||||
points: graphicTypePoints[0],
|
||||
},
|
||||
cachedStats: {},
|
||||
frameNumber,
|
||||
renderableData,
|
||||
TrackingUniqueIdentifier,
|
||||
labels: measurement.labels,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* const annotationManager = annotation.annotationState.getAnnotationManager();
|
||||
* was not triggering annotation_added events.
|
||||
*/
|
||||
annotation.state.addAnnotation(SRAnnotation);
|
||||
console.debug('Adding SR annotation:', SRAnnotation);
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { addTool } from '@cornerstonejs/tools';
|
||||
|
||||
export default function addToolInstance(name: string, toolClass, configuration?): void {
|
||||
export default function addToolInstance(name: string, toolClass, configuration = {}): void {
|
||||
class InstanceClass extends toolClass {
|
||||
static toolName = name;
|
||||
constructor(toolProps, defaultToolProps) {
|
||||
toolProps.configuration = toolProps.configuration
|
||||
? { ...toolProps.configuration, ...configuration }
|
||||
: configuration;
|
||||
super(toolProps, defaultToolProps);
|
||||
}
|
||||
}
|
||||
addTool(InstanceClass);
|
||||
}
|
||||
@@ -61,6 +61,11 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
|
||||
|
||||
const imageSet = new ImageSet(instances);
|
||||
const instance = instances[0];
|
||||
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageSet.setAttributes({
|
||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||
SeriesDate: instance.SeriesDate,
|
||||
|
||||
@@ -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,3 +1,7 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
/**
|
||||
* Extracts the label from the toolData imported from dcmjs. We need to do this
|
||||
* as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools.
|
||||
@@ -9,13 +13,15 @@
|
||||
export default function getLabelFromDCMJSImportedToolData(toolData) {
|
||||
const { findingSites = [], finding } = toolData;
|
||||
|
||||
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
|
||||
let freeTextLabel = findingSites.find(
|
||||
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
||||
);
|
||||
|
||||
if (freeTextLabel) {
|
||||
return freeTextLabel.CodeMeaning;
|
||||
}
|
||||
|
||||
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
|
||||
if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) {
|
||||
return finding.CodeMeaning;
|
||||
}
|
||||
}
|
||||
+36
-116
@@ -1,124 +1,49 @@
|
||||
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';
|
||||
|
||||
import { SCOORDTypes } from '../enums';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
const toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
const measurementData = {
|
||||
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
|
||||
renderableData: {},
|
||||
labels: measurement.labels,
|
||||
imageId,
|
||||
};
|
||||
|
||||
measurement.coords.forEach(coord => {
|
||||
const { GraphicType, GraphicData } = coord;
|
||||
|
||||
if (measurementData.renderableData[GraphicType] === undefined) {
|
||||
measurementData.renderableData[GraphicType] = [];
|
||||
const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => {
|
||||
const renderableData = [];
|
||||
if (ValueType === 'SCOORD3D') {
|
||||
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||
renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]);
|
||||
renderableData.push(worldPos);
|
||||
}
|
||||
}
|
||||
return renderableData;
|
||||
};
|
||||
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
|
||||
);
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
const SRAnnotation: Types.Annotation = {
|
||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||
metadata: {
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
toolName: toolName,
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
label: measurement.labels,
|
||||
handles: {
|
||||
textBox: {},
|
||||
},
|
||||
cachedStats: {
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
},
|
||||
frameNumber: frameNumber,
|
||||
},
|
||||
};
|
||||
|
||||
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(':');
|
||||
|
||||
let renderableData: csTypes.Point3[];
|
||||
function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
|
||||
let renderableData = [];
|
||||
|
||||
switch (GraphicType) {
|
||||
case SCOORD_TYPES.POINT:
|
||||
case SCOORD_TYPES.MULTIPOINT:
|
||||
case SCOORD_TYPES.POLYLINE:
|
||||
renderableData = [];
|
||||
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||
GraphicData[i],
|
||||
GraphicData[i + 1],
|
||||
]);
|
||||
|
||||
renderableData.push(worldPos);
|
||||
}
|
||||
|
||||
case SCOORDTypes.POINT:
|
||||
case SCOORDTypes.MULTIPOINT:
|
||||
case SCOORDTypes.POLYLINE: {
|
||||
renderableData = getRenderableCoords({ GraphicData, ValueType, imageId });
|
||||
break;
|
||||
case SCOORD_TYPES.CIRCLE: {
|
||||
const pointsWorld = [];
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||
GraphicData[i],
|
||||
GraphicData[i + 1],
|
||||
]);
|
||||
|
||||
pointsWorld.push(worldPos);
|
||||
}
|
||||
|
||||
}
|
||||
case SCOORDTypes.CIRCLE: {
|
||||
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
|
||||
GraphicData,
|
||||
ValueType,
|
||||
imageId,
|
||||
});
|
||||
// We do not have an explicit draw circle svg helper in Cornerstone3D at
|
||||
// this time, but we can use the ellipse svg helper to draw a circle, so
|
||||
// here we reshape the data for that purpose.
|
||||
const center = pointsWorld[0];
|
||||
const onPerimeter = pointsWorld[1];
|
||||
|
||||
const radius = vec3.distance(center, onPerimeter);
|
||||
|
||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||
|
||||
if (!imagePlaneModule) {
|
||||
throw new Error('No imagePlaneModule found');
|
||||
}
|
||||
@@ -133,14 +58,12 @@ function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifie
|
||||
|
||||
// we need to get major/minor axis (which are both the same size major = minor)
|
||||
|
||||
// first axisStart
|
||||
const firstAxisStart = vec3.create();
|
||||
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
|
||||
|
||||
const firstAxisEnd = vec3.create();
|
||||
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
|
||||
|
||||
// second axisStart
|
||||
const secondAxisStart = vec3.create();
|
||||
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
|
||||
|
||||
@@ -156,21 +79,16 @@ function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifie
|
||||
|
||||
break;
|
||||
}
|
||||
case SCOORD_TYPES.ELLIPSE: {
|
||||
case SCOORDTypes.ELLIPSE: {
|
||||
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
|
||||
// But Cornerstone3D points are ordered as top, bottom, left, right for the
|
||||
// ellipse so we need to identify if the majorAxis is horizontal or vertical
|
||||
// and then choose the correct points to use for the ellipse.
|
||||
|
||||
const pointsWorld: csTypes.Point3[] = [];
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||
GraphicData[i],
|
||||
GraphicData[i + 1],
|
||||
]);
|
||||
|
||||
pointsWorld.push(worldPos);
|
||||
}
|
||||
const pointsWorld: csTypes.Point3[] = getRenderableCoords({
|
||||
GraphicData,
|
||||
ValueType,
|
||||
imageId,
|
||||
});
|
||||
|
||||
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
|
||||
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
|
||||
@@ -220,3 +138,5 @@ function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifie
|
||||
|
||||
return renderableData;
|
||||
}
|
||||
|
||||
export default getRenderableData;
|
||||
+24
-8
@@ -3,14 +3,12 @@ import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
|
||||
const { locking } = CsAnnotation;
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
|
||||
const { locking } = CsAnnotation;
|
||||
const { guid } = OHIF.utils;
|
||||
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
|
||||
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
|
||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
const convertCode = (codingValues, code) => {
|
||||
@@ -43,15 +41,23 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
const disableEditing = appConfig?.disableEditing;
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||
|
||||
const codingValues = customizationService.getCustomization('codingValues', {});
|
||||
|
||||
const { disableEditing } = customizationService.getCustomization(
|
||||
'PanelMeasurement.disableEditing',
|
||||
{
|
||||
id: 'default.disableEditing',
|
||||
disableEditing: false,
|
||||
}
|
||||
);
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// TODO -> We should define a strict versioning somewhere.
|
||||
@@ -90,7 +96,7 @@ export default function hydrateStructuredReport(
|
||||
const datasetToUse = _mapLegacyDataSet(instance);
|
||||
|
||||
// Use dcmjs to generate toolState.
|
||||
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||
datasetToUse,
|
||||
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
|
||||
// for the imageToWorld transformation. The following assumes that the order
|
||||
@@ -101,6 +107,16 @@ export default function hydrateStructuredReport(
|
||||
metaData
|
||||
);
|
||||
|
||||
const onBeforeSRHydration =
|
||||
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
|
||||
|
||||
if (typeof onBeforeSRHydration === 'function') {
|
||||
storedMeasurementByAnnotationType = onBeforeSRHydration({
|
||||
storedMeasurementByAnnotationType,
|
||||
displaySet,
|
||||
});
|
||||
}
|
||||
|
||||
// Filter what is found by DICOM SR to measurements we support.
|
||||
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||
const hydratableMeasurementsInSR = {};
|
||||
@@ -4,7 +4,7 @@ const cornerstoneAdapters =
|
||||
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
|
||||
const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG;
|
||||
|
||||
/**
|
||||
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
|
||||
|
||||
@@ -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,49 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.9.0",
|
||||
"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",
|
||||
"@ohif/extension-cornerstone": "3.9.0",
|
||||
"@ohif/extension-default": "3.9.0",
|
||||
"@ohif/i18n": "3.9.0",
|
||||
"@ohif/ui": "3.9.0",
|
||||
"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": "^2.2.3",
|
||||
"@cornerstonejs/tools": "^2.2.3",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||
|
||||
export { updateSegmentationsChartDisplaySet };
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
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,
|
||||
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||
) {
|
||||
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
|
||||
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 segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||
const maskVolumeId = segmentationVolume?.volumeId;
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
|
||||
if (pixelCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
|
||||
// segmentationRepresentationUID,
|
||||
// 1 // segmentIndex
|
||||
// );
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
|
||||
|
||||
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
|
||||
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, { servicesManager }) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
|
||||
);
|
||||
|
||||
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 {
|
||||
debugger;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
|
||||
|
||||
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,412 @@
|
||||
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 => {
|
||||
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||
maskVolumeId: volume.volumeId,
|
||||
}) 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 = [];
|
||||
let sum = 0;
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
let minIndex = 0;
|
||||
let maxIndex = 0;
|
||||
|
||||
// Single pass through the data to collect all needed values
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
const value = timeData[voxelIndex][timeIndex];
|
||||
voxelValues.push(value);
|
||||
sum += value;
|
||||
|
||||
if (value < minValue) {
|
||||
minValue = value;
|
||||
minIndex = voxelIndex;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
maxValue = value;
|
||||
maxIndex = voxelIndex;
|
||||
}
|
||||
}
|
||||
|
||||
mean.push(sum / numVoxels);
|
||||
min.push(minValue);
|
||||
minIJK.push(ijkCoords[minIndex]);
|
||||
max.push(maxValue);
|
||||
maxIJK.push(ijkCoords[maxIndex]);
|
||||
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 displaySet = displaySetService.getDisplaySetByUID(
|
||||
referenceDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
label,
|
||||
});
|
||||
|
||||
const firstViewport = viewports.values().next().value;
|
||||
|
||||
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
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,681 @@
|
||||
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.8 },
|
||||
{ value: 1, opacity: 0.8 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
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,61 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicSegmentation = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
<PanelSegmentation
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={configuration}
|
||||
>
|
||||
<DynamicExport
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PanelSegmentation>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dynamic-volume',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-segmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedDynamicSegmentation,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getPanelModule;
|
||||
Loaded 100 of 2454 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user