Compare commits
848
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522531cc5c | ||
|
|
8c89d9472c | ||
|
|
9d94e26d5d | ||
|
|
974ef29aa1 | ||
|
|
a4d1f56c01 | ||
|
|
35bc074a53 | ||
|
|
4d74611eb1 | ||
|
|
79cfc58533 | ||
|
|
735405a855 | ||
|
|
af40f06524 | ||
|
|
568cd0b39d | ||
|
|
8643c868fd | ||
|
|
eede2945ae | ||
|
|
1fc38c7379 | ||
|
|
271b84f78f | ||
|
|
dd896fea25 | ||
|
|
1e3f69d977 | ||
|
|
341f1c4ce7 | ||
|
|
b8ff0a80f0 | ||
|
|
760194ac80 | ||
|
|
d24fe54d2b | ||
|
|
1cc78112e5 | ||
|
|
0f816e4733 | ||
|
|
04dcacd3a5 | ||
|
|
47e231f061 | ||
|
|
4b7c26e273 | ||
|
|
d45dc473ad | ||
|
|
c766f3c054 | ||
|
|
03941dca8e | ||
|
|
be6e228c36 | ||
|
|
570e087007 | ||
|
|
afac23a79a | ||
|
|
3322f652fe | ||
|
|
d0cf9e02f4 | ||
|
|
84e3e43ae3 | ||
|
|
71b685364f | ||
|
|
3c8446a5e5 | ||
|
|
4e6530d1d8 | ||
|
|
0b87691d50 | ||
|
|
730046f83c | ||
|
|
763002f2fe | ||
|
|
76e630cf61 | ||
|
|
428b6ffbe7 | ||
|
|
78597aacac | ||
|
|
eacebad315 | ||
|
|
c8261d9883 | ||
|
|
eedaa9a2e2 | ||
|
|
fa37acc2fb | ||
|
|
05526e5cdc | ||
|
|
cf2e58bdb9 | ||
|
|
9b953f0942 | ||
|
|
75cc6eff32 | ||
|
|
c271e869c2 | ||
|
|
b71b14efdd | ||
|
|
7ffe17deea | ||
|
|
dd2e89bfdb | ||
|
|
18f8c88a89 | ||
|
|
bd3ff5f57c | ||
|
|
04a599962d | ||
|
|
3be5d84fc4 | ||
|
|
83d4bc4a5a | ||
|
|
83ea0ad93e | ||
|
|
b77b2f3fa0 | ||
|
|
111c3b3d0d | ||
|
|
76a2b90c17 | ||
|
|
aa83cda49c | ||
|
|
1ae4967fe5 | ||
|
|
12de7a85c9 | ||
|
|
91d0ae7da6 | ||
|
|
f7fab0c461 | ||
|
|
bde0eb1f07 | ||
|
|
73ee3140ea | ||
|
|
2a738dec09 | ||
|
|
3d5ee687cb | ||
|
|
7060aeca4b | ||
|
|
5ecb8e9821 | ||
|
|
ed3de96e4c | ||
|
|
db8a54d965 | ||
|
|
3980150aef | ||
|
|
3d4ec3181d | ||
|
|
779907e526 | ||
|
|
1841d6202f | ||
|
|
6a4ca70836 | ||
|
|
8dda7d5f81 | ||
|
|
e207f58031 | ||
|
|
faef74ab21 | ||
|
|
3ff9a65ef3 | ||
|
|
26d77fb149 | ||
|
|
5977326665 | ||
|
|
27e4f0440c | ||
|
|
f38805e6f1 | ||
|
|
a7892c0d59 | ||
|
|
991f17e0ca | ||
|
|
522d2aa764 | ||
|
|
fceb515a68 | ||
|
|
e1fb97f862 | ||
|
|
e0289dc49f | ||
|
|
302705ea4d | ||
|
|
ff0568f2ab | ||
|
|
545411137c | ||
|
|
1f33b291b9 | ||
|
|
768e5ba928 | ||
|
|
c175837273 | ||
|
|
529806c0d9 | ||
|
|
658d824505 | ||
|
|
94bdcf414d | ||
|
|
a24ec13239 | ||
|
|
2c7c62443c | ||
|
|
540923ad8b | ||
|
|
df926e2513 | ||
|
|
86ad50d886 | ||
|
|
0ed9ef06c1 | ||
|
|
4a59126dd9 | ||
|
|
277cef2168 | ||
|
|
168d5b27f3 | ||
|
|
fd5bcee909 | ||
|
|
8bbf83fb84 | ||
|
|
4253982050 | ||
|
|
10d93161b4 | ||
|
|
7d3e222182 | ||
|
|
46df245fb8 | ||
|
|
4d3ff5a6cc | ||
|
|
4e99f1ed34 | ||
|
|
2ddeed981d | ||
|
|
eeaf49c61a | ||
|
|
2c1e8cb3e1 | ||
|
|
f772be8ae3 | ||
|
|
4c5e34d183 | ||
|
|
0c62bd5e4e | ||
|
|
4424d2a2bc | ||
|
|
48473625f6 | ||
|
|
32ab291305 | ||
|
|
93eb0dee19 | ||
|
|
ac852485cd | ||
|
|
d19cd6ec7b | ||
|
|
4556dd9713 | ||
|
|
7b2e37a44f | ||
|
|
64a634cab9 | ||
|
|
ca7defc668 | ||
|
|
e5b95de0b0 | ||
|
|
f08cc303e6 | ||
|
|
b8b14b8940 | ||
|
|
7f19db7f0e | ||
|
|
e277303395 | ||
|
|
f7cf356d46 | ||
|
|
baad251259 | ||
|
|
24e0263fb9 | ||
|
|
17ed63eb1d | ||
|
|
6f8592a449 | ||
|
|
c112db13a8 | ||
|
|
b0704b88cd | ||
|
|
8491407319 | ||
|
|
4f6f5254fb | ||
|
|
77758fde4f | ||
|
|
90943b4dc5 | ||
|
|
4bddd53fcd | ||
|
|
c2b75e1a50 | ||
|
|
51f17a92c1 | ||
|
|
c2624f116d | ||
|
|
9a81f8cef5 | ||
|
|
8a35453100 | ||
|
|
b660a5d523 | ||
|
|
da8ecdd88c | ||
|
|
ef5ce6c19b | ||
|
|
c063fe8550 | ||
|
|
5524d1e845 | ||
|
|
27eb442f62 | ||
|
|
9824f79177 | ||
|
|
e07bb212bb | ||
|
|
2a06a3c17f | ||
|
|
4c1356cab7 | ||
|
|
a17fb5aeba | ||
|
|
b919ebbc41 | ||
|
|
ec0c4f7451 | ||
|
|
746781f6a4 | ||
|
|
332d498d2c | ||
|
|
6c9d99eef8 | ||
|
|
57f07ad9f2 | ||
|
|
b1ad3787e4 | ||
|
|
226f9b0773 | ||
|
|
9b8eef9474 | ||
|
|
1e605027ff | ||
|
|
b48aab263f | ||
|
|
08d978bd75 | ||
|
|
4857e0002b | ||
|
|
9ed2d88cb1 | ||
|
|
d6d3fa7ef4 | ||
|
|
3f1ea26eba | ||
|
|
9f80732674 | ||
|
|
268d62e991 | ||
|
|
f6337e0970 | ||
|
|
2ed6172861 | ||
|
|
dc829c8fca | ||
|
|
afc2ca0dce | ||
|
|
abb6da4458 | ||
|
|
5311c941f7 | ||
|
|
4199d788a8 | ||
|
|
40eac04428 | ||
|
|
abaf6582e0 | ||
|
|
ce70c8401a | ||
|
|
a906eb04ab | ||
|
|
d3c9ff50c7 | ||
|
|
4da7fc307f | ||
|
|
68bbe0b1a5 | ||
|
|
d15494f80c | ||
|
|
75271ca416 | ||
|
|
a83bd298f5 | ||
|
|
8ff59dacb5 | ||
|
|
26fd51f535 | ||
|
|
10ef509ff1 | ||
|
|
735a13a85b | ||
|
|
a361e556ef | ||
|
|
9baf153d0d | ||
|
|
b99f64da1b | ||
|
|
b194f672c0 | ||
|
|
635877f184 | ||
|
|
2306ec2a6d | ||
|
|
0afb3fc753 | ||
|
|
2234121a19 | ||
|
|
a1ecc826aa | ||
|
|
1a6aa09a77 | ||
|
|
2e7124bace | ||
|
|
2ddfc95bfa | ||
|
|
316d5a7644 | ||
|
|
01c7d45c15 | ||
|
|
e804d7473e | ||
|
|
38ed7ba744 | ||
|
|
a1fb7217bd | ||
|
|
8adbeb54f9 | ||
|
|
8b2f80b82e | ||
|
|
31e19ffe8c | ||
|
|
4f6c8766b1 | ||
|
|
5891505c51 | ||
|
|
9afded1720 | ||
|
|
60e6e686ec | ||
|
|
c6327acdc1 | ||
|
|
773d6edaf7 | ||
|
|
843d99d9ef | ||
|
|
c88bdac4b1 | ||
|
|
a8d438d8f7 | ||
|
|
fec45860b8 | ||
|
|
b9ca76ea67 | ||
|
|
ffc7d2bd6b | ||
|
|
1d91871d2c | ||
|
|
ebfa1ff5f2 | ||
|
|
1873a8405f | ||
|
|
df35cb55b6 | ||
|
|
9247d07ccd | ||
|
|
14f5f0c074 | ||
|
|
c2cc0832f3 | ||
|
|
10d64322bf | ||
|
|
cb814e6a60 | ||
|
|
fa9bbe3afc | ||
|
|
7f2f739b1b | ||
|
|
dcde841839 | ||
|
|
c784b7e6d5 | ||
|
|
0d8f43fa30 | ||
|
|
9b1b501d83 | ||
|
|
c6b28654b5 | ||
|
|
fb1c2673a8 | ||
|
|
b6b06ed09e | ||
|
|
338bd865c3 | ||
|
|
cf1018c779 | ||
|
|
0f12fa16bc | ||
|
|
04ab79f8fb | ||
|
|
1fd39626e7 | ||
|
|
5e406a8c93 | ||
|
|
7c15bb8901 | ||
|
|
f71b88e639 | ||
|
|
faaaccdfa7 | ||
|
|
da51e6e3c1 | ||
|
|
4a91dfdfa5 | ||
|
|
c2534198ad | ||
|
|
950453fd70 | ||
|
|
f4ba9b3cc6 | ||
|
|
7610b4c6a5 | ||
|
|
349306da40 | ||
|
|
66322c68a2 | ||
|
|
4e03f34fd4 | ||
|
|
1445e12124 | ||
|
|
f14d9ebdd7 | ||
|
|
a3bf972177 | ||
|
|
578c3939f0 | ||
|
|
dee6909a99 | ||
|
|
12094cc77a | ||
|
|
a5ea3e15c7 | ||
|
|
f412dc254b | ||
|
|
c0ed128fb7 | ||
|
|
c829270c80 | ||
|
|
a16d612a8a | ||
|
|
5f94ebe81a | ||
|
|
8218a9d034 | ||
|
|
4b94f18f7c | ||
|
|
8e7e9ca02f | ||
|
|
cd4e6408bc | ||
|
|
c1fced0573 | ||
|
|
e41019f2b3 | ||
|
|
383f45823c | ||
|
|
6337d2e9cb | ||
|
|
e987710908 | ||
|
|
7dbe5e37b0 | ||
|
|
ff974e2a03 | ||
|
|
cccf74d6ea | ||
|
|
4be8495335 | ||
|
|
1cb8d328a4 | ||
|
|
2c8dda01ae | ||
|
|
4e185e4b21 | ||
|
|
4732e8093e | ||
|
|
984471e972 | ||
|
|
25cb71b3f6 | ||
|
|
0e75f6d62c | ||
|
|
d75b3e25b6 | ||
|
|
0bb9b17636 | ||
|
|
b21ee9bc5b | ||
|
|
7ed0f6d53b | ||
|
|
98f8187dfe | ||
|
|
4643d6dc40 | ||
|
|
258b353269 | ||
|
|
0f39496290 | ||
|
|
325cb3f7d5 | ||
|
|
088b563bf6 | ||
|
|
09ac43f297 | ||
|
|
d59828ab22 | ||
|
|
5f04b0f3be | ||
|
|
bdb847a6eb | ||
|
|
0052bf1ae3 | ||
|
|
41815ee2af | ||
|
|
56fdb82812 | ||
|
|
fcde5ddbce | ||
|
|
7818f406d8 | ||
|
|
329d5b6237 | ||
|
|
3b8faa8a76 | ||
|
|
59136e5435 | ||
|
|
88fccda491 | ||
|
|
5688a2b4b8 | ||
|
|
f02ce22ae3 | ||
|
|
ff5a152270 | ||
|
|
082014a694 | ||
|
|
194b4971c2 | ||
|
|
d6c7b9c23c | ||
|
|
b82465344c | ||
|
|
f8cb810b92 | ||
|
|
dbda78df15 | ||
|
|
66a16cd22f | ||
|
|
5a9fcd4bf0 | ||
|
|
69cddff628 | ||
|
|
08c14e9ba2 | ||
|
|
7fd522ab68 | ||
|
|
df74557866 | ||
|
|
69063a41b7 | ||
|
|
41fb597085 | ||
|
|
a3a35471a9 | ||
|
|
a0bcac15cd | ||
|
|
ac7875128c | ||
|
|
bd02205ef9 | ||
|
|
9535422361 | ||
|
|
a13380407b | ||
|
|
1a0d7a930c | ||
|
|
91117f40f8 | ||
|
|
a41ed3bade | ||
|
|
f12f2bfec5 | ||
|
|
4a87302f06 | ||
|
|
5dae147155 | ||
|
|
4952dab010 | ||
|
|
5425308e55 | ||
|
|
c50298f394 | ||
|
|
a82ebeebb2 | ||
|
|
ed94df0dfe | ||
|
|
4926a9532c | ||
|
|
7a36eba67c | ||
|
|
8a08d26f01 | ||
|
|
de598cdc1f | ||
|
|
a9b180c5d4 | ||
|
|
ffc7ab6845 | ||
|
|
1714aa8ec7 | ||
|
|
ce0d152f92 | ||
|
|
16bcc06f15 | ||
|
|
bb42fd2a8d | ||
|
|
e37168094a | ||
|
|
e1e9b5ccb1 | ||
|
|
6b7e73b78a | ||
|
|
43bc142439 | ||
|
|
c05ec1909f | ||
|
|
c7529394ad | ||
|
|
f6f60a63d2 | ||
|
|
0d2ccc5830 | ||
|
|
08be5cc656 | ||
|
|
c453c29449 | ||
|
|
a69dd8b1a0 | ||
|
|
17c5642a59 | ||
|
|
bd2f843469 | ||
|
|
8b9b02d84c | ||
|
|
c9958bafad | ||
|
|
85f465968b | ||
|
|
47e491f1c8 | ||
|
|
e56b53e897 | ||
|
|
2bdf4afa4d | ||
|
|
1b8729dded | ||
|
|
f71d370969 | ||
|
|
033831ee72 | ||
|
|
49c08298fc | ||
|
|
cfa202e89a | ||
|
|
391a5f3bdd | ||
|
|
64d65a6830 | ||
|
|
3653de7487 | ||
|
|
a8ac297e16 | ||
|
|
4e09f85d5d | ||
|
|
d2f187153d | ||
|
|
62e5a62066 | ||
|
|
7b40963997 | ||
|
|
75493334cf | ||
|
|
746f2524eb | ||
|
|
0eb9ff235c | ||
|
|
e9b95afd4c | ||
|
|
fc33bd0d59 | ||
|
|
479e280fdf | ||
|
|
d4f81cd2b7 | ||
|
|
54fded554e | ||
|
|
c0391cea1b | ||
|
|
a9e69eb36f | ||
|
|
a52be6460b | ||
|
|
587b6423cb | ||
|
|
f52a32d6c5 | ||
|
|
8142c55b6a | ||
|
|
14afb42e2c | ||
|
|
4e8e4f1491 | ||
|
|
814a2c914b | ||
|
|
f4fb42adb2 | ||
|
|
0fee0236c2 | ||
|
|
e7fe879318 | ||
|
|
a619634678 | ||
|
|
651a7dc574 | ||
|
|
8432d5f633 | ||
|
|
1dc00d3030 | ||
|
|
44d450c5b8 | ||
|
|
c75a3f3af5 | ||
|
|
908482d2e0 | ||
|
|
6b26db387e | ||
|
|
b7419fb6a1 | ||
|
|
c4045251fb | ||
|
|
ec4cc64b06 | ||
|
|
d9473e8db2 | ||
|
|
7f3fb9bb61 | ||
|
|
8cf5b52b84 | ||
|
|
d7bd36544a | ||
|
|
eafa2efda5 | ||
|
|
3f06c966eb | ||
|
|
951f95ef0a | ||
|
|
5737935a96 | ||
|
|
459b0d223a | ||
|
|
c368d763b0 | ||
|
|
6b6f49b867 | ||
|
|
892d03fc3c | ||
|
|
2acc878f26 | ||
|
|
1df91fd104 | ||
|
|
9bde644fce | ||
|
|
7f8fa1e1f4 | ||
|
|
dce9c6b7c3 | ||
|
|
467acbd997 | ||
|
|
f90184d9d9 | ||
|
|
cc9720dca2 | ||
|
|
4dd485bd2d | ||
|
|
209d81f475 | ||
|
|
5c319ce4f0 | ||
|
|
f6fa509874 | ||
|
|
8e47112d8e | ||
|
|
165c9dc710 | ||
|
|
8faa9bf851 | ||
|
|
246c10da07 | ||
|
|
782a6a0c88 | ||
|
|
5293253ecd | ||
|
|
7f86ca1a2b | ||
|
|
01c3726ced | ||
|
|
f73c485821 | ||
|
|
30a3965da7 | ||
|
|
df8efba82c | ||
|
|
0522d58d9b | ||
|
|
576ad47ebe | ||
|
|
5f112337b6 | ||
|
|
20042f9109 | ||
|
|
d59b9d88fa | ||
|
|
7f229d5c23 | ||
|
|
eac0308275 | ||
|
|
c2f9252aef | ||
|
|
55d6393a3f | ||
|
|
5a6c2aeed9 | ||
|
|
751919c27b | ||
|
|
fb93ef9219 | ||
|
|
a0a3a30361 | ||
|
|
64b196db20 | ||
|
|
c227c5742a | ||
|
|
2c75638d1c | ||
|
|
bf27da4174 | ||
|
|
ec79b5523e | ||
|
|
694f3e9611 | ||
|
|
d33b1ade87 | ||
|
|
abab3dc7ad | ||
|
|
b7fbf04230 | ||
|
|
ec71721d49 | ||
|
|
a265257569 | ||
|
|
951c21fda8 | ||
|
|
f916b2f40e | ||
|
|
841320adfb | ||
|
|
10976c2a48 | ||
|
|
e2ab118217 | ||
|
|
8cd48c7f70 | ||
|
|
fd2f88920b | ||
|
|
fdd33aea0f | ||
|
|
ebe6fbaf31 | ||
|
|
bfa76de93b | ||
|
|
56c7c27ad9 | ||
|
|
3f70617c62 | ||
|
|
1f634bab45 | ||
|
|
f6ad3446ad | ||
|
|
a6059363b1 | ||
|
|
834be4aaf1 | ||
|
|
f15022c6e9 | ||
|
|
a5fb53fbcf | ||
|
|
d9349ead6f | ||
|
|
9d7c2abdef | ||
|
|
18ef2562a1 | ||
|
|
17d2889dc5 | ||
|
|
2109b2c1e6 | ||
|
|
19fb355c50 | ||
|
|
cc5cdfb081 | ||
|
|
86c12246ac | ||
|
|
022313302e | ||
|
|
5a7f7841b2 | ||
|
|
9b6af66859 | ||
|
|
cdfe5c7bef | ||
|
|
a3b51aa8db | ||
|
|
62b4fa3e89 | ||
|
|
c757d69671 | ||
|
|
24567c4e92 | ||
|
|
62abb3ac47 | ||
|
|
f7a0763000 | ||
|
|
d25809eed0 | ||
|
|
6b0bb9402f | ||
|
|
9cbf82167e | ||
|
|
7f0cc0f60e | ||
|
|
7d67834159 | ||
|
|
51f9407d93 | ||
|
|
fdb073c216 | ||
|
|
d319d20b0d | ||
|
|
8a977bb70c | ||
|
|
9233143b9d | ||
|
|
1f5c60bbda | ||
|
|
ca22bac851 | ||
|
|
32fc6f0d4b | ||
|
|
e2491daff7 | ||
|
|
7dd0260f86 | ||
|
|
4b63d48518 | ||
|
|
4ecffe89cb | ||
|
|
9fb3653281 | ||
|
|
06660be7c4 | ||
|
|
5d5eafb0f5 | ||
|
|
0abb62167a | ||
|
|
3e6913b097 | ||
|
|
4559d39a91 | ||
|
|
b7ddbccdff | ||
|
|
e480e841bb | ||
|
|
afd528b8d7 | ||
|
|
76da2d86a9 | ||
|
|
5527c0e8ea | ||
|
|
bda98b0bee | ||
|
|
74d96d378d | ||
|
|
94496f72b9 | ||
|
|
338e386948 | ||
|
|
0531bee83b | ||
|
|
6411488cd1 | ||
|
|
55f0b54db1 | ||
|
|
2e70f0ffa1 | ||
|
|
0931356f68 | ||
|
|
b7cd0c6027 | ||
|
|
8d83c117cf | ||
|
|
bd93a6f428 | ||
|
|
125f11fc73 | ||
|
|
ce3bc8962d | ||
|
|
0d8820d941 | ||
|
|
c61dbba8d0 | ||
|
|
2410c6a509 | ||
|
|
d26c9551b6 | ||
|
|
b6afba12a5 | ||
|
|
4e16099ad3 | ||
|
|
887a892a5b | ||
|
|
db4d94c3fe | ||
|
|
0b5836ca1a | ||
|
|
8b969eb1e9 | ||
|
|
ac30be6038 | ||
|
|
d8a6e79df0 | ||
|
|
5712e91ca1 | ||
|
|
a3b1251386 | ||
|
|
2fbaa11cf1 | ||
|
|
8fe7b78e54 | ||
|
|
d9a3ba7990 | ||
|
|
2ca9f2b0cc | ||
|
|
559f248d1f | ||
|
|
084a10f783 | ||
|
|
5043ea3260 | ||
|
|
e0ec1011cf | ||
|
|
bbf1a19676 | ||
|
|
9a3d832cc2 | ||
|
|
e373e22481 | ||
|
|
149d6d049c | ||
|
|
4d06a7d286 | ||
|
|
a7e4204cea | ||
|
|
8996df80e4 | ||
|
|
d5dbe20256 | ||
|
|
105fd7a468 | ||
|
|
2c183aa4a7 | ||
|
|
8788dae6a4 | ||
|
|
6937129991 | ||
|
|
8acbd760d8 | ||
|
|
c65e7f57cc | ||
|
|
5e09b405f4 | ||
|
|
12d3db2dbc | ||
|
|
ae9a238289 | ||
|
|
691accacd9 | ||
|
|
e035ef1dcc | ||
|
|
43aa20c372 | ||
|
|
860f76586a | ||
|
|
9b8e5cfd1a | ||
|
|
613401e043 | ||
|
|
47b9a06f74 | ||
|
|
f15eb44b4c | ||
|
|
8f79ebafd9 | ||
|
|
a3c7c2c3da | ||
|
|
32017d15a4 | ||
|
|
e3967e62f2 | ||
|
|
09f9024728 | ||
|
|
0cb6106d0f | ||
|
|
4dc26ae603 | ||
|
|
9850f92b8a | ||
|
|
f10683c667 | ||
|
|
2bfc790870 | ||
|
|
3f08a76aac | ||
|
|
a8d84443a9 | ||
|
|
ac5ae4a29e | ||
|
|
6d9c701fa9 | ||
|
|
054b262e9c | ||
|
|
3dac0f9e73 | ||
|
|
d62c32c898 | ||
|
|
d554f02f7c | ||
|
|
ff6dd3a152 | ||
|
|
a22818c1f4 | ||
|
|
6abb095b8a | ||
|
|
fe658c6392 | ||
|
|
70c94e8d5a | ||
|
|
5addf51c45 | ||
|
|
8e3e2085d4 | ||
|
|
078a72996f | ||
|
|
dc14a48fd9 | ||
|
|
61699d00b6 | ||
|
|
eb854ac2cb | ||
|
|
6c42f269a7 | ||
|
|
ac6e674b4d | ||
|
|
316cf0a7bd | ||
|
|
c011236060 | ||
|
|
2ed7a1adfa | ||
|
|
492150478b | ||
|
|
cb03077d37 | ||
|
|
28fb921089 | ||
|
|
bc175d68b6 | ||
|
|
884020f143 | ||
|
|
d6f0092a32 | ||
|
|
01fa5d83a6 | ||
|
|
625919d4e4 | ||
|
|
55ad8efbab | ||
|
|
fab52808cb | ||
|
|
f5ad0de2d2 | ||
|
|
d9abc3da8d | ||
|
|
9d07705a1f | ||
|
|
0966f62310 | ||
|
|
f00d18292f | ||
|
|
3628ad84c8 | ||
|
|
b21287e908 | ||
|
|
d90a4cfb16 | ||
|
|
a518854a4c | ||
|
|
28f86f3cd6 | ||
|
|
cf97fa9b7b | ||
|
|
ad7ade28ea | ||
|
|
9aed7b8486 | ||
|
|
8aedb2ec54 | ||
|
|
cbe926f52b | ||
|
|
855648f41f | ||
|
|
bf587070cc | ||
|
|
953f36ab4c | ||
|
|
0911e3c0d1 | ||
|
|
464148ece6 | ||
|
|
544dc543d9 | ||
|
|
f5dcf25d98 | ||
|
|
7734296d1d | ||
|
|
9e047784d1 | ||
|
|
df96a4992c | ||
|
|
19931614dc | ||
|
|
ef477c29c5 | ||
|
|
6c8ebc1d5a | ||
|
|
ec4b5a6876 | ||
|
|
9f8df95f3e | ||
|
|
c718b20659 | ||
|
|
cded082617 | ||
|
|
27c040fdda | ||
|
|
e923bf2bc5 | ||
|
|
2d611d06ed | ||
|
|
c901a847af | ||
|
|
7662157cbf | ||
|
|
161c84d875 | ||
|
|
07c628e689 | ||
|
|
a4a6de07f4 | ||
|
|
daf18f3e6b | ||
|
|
9832dbe653 | ||
|
|
f4df631a4a | ||
|
|
bdfaa35f7f | ||
|
|
9b04020a87 | ||
|
|
7a13c28eb3 | ||
|
|
b9fb0ce96f | ||
|
|
d4a4267429 | ||
|
|
847f1da8e0 | ||
|
|
71ccbe87c1 | ||
|
|
a2d34c97ed | ||
|
|
ee76912fed | ||
|
|
e94e8439ad | ||
|
|
01924b8bf2 | ||
|
|
7d22480036 | ||
|
|
4e70ee7894 | ||
|
|
64a72774a8 | ||
|
|
1b56b48d7b | ||
|
|
41e870b7db | ||
|
|
ebbc37d291 | ||
|
|
29b9dc58ba | ||
|
|
409e06d0c6 | ||
|
|
82440e88d5 | ||
|
|
bc73de9710 | ||
|
|
ba8f8ba2d6 | ||
|
|
8e2c607904 | ||
|
|
98f4ac2e75 | ||
|
|
192d8e69a9 | ||
|
|
aef68d18b8 | ||
|
|
2dd9b893ad | ||
|
|
2ebd03de07 | ||
|
|
e0088ec918 | ||
|
|
3c848c6d34 | ||
|
|
c931b63d5e | ||
|
|
38edde5e37 | ||
|
|
66eebbed0f | ||
|
|
8ecbca700c | ||
|
|
1223dd00e1 | ||
|
|
3c6a5ef69e | ||
|
|
dd5251ac95 | ||
|
|
71c0c18a8a | ||
|
|
5f6f528fbf | ||
|
|
60547b507f | ||
|
|
f8b7687f94 | ||
|
|
230ae9758b | ||
|
|
47de38f3ff | ||
|
|
5b454472e4 | ||
|
|
2f5076ece8 | ||
|
|
401d5dcedd | ||
|
|
464976f035 | ||
|
|
b54f2075ec | ||
|
|
03ad79eaed | ||
|
|
e993b7f9e1 | ||
|
|
8e12021939 | ||
|
|
8ad5d559bb | ||
|
|
20a4daa54e | ||
|
|
836e67a6ab | ||
|
|
05e3f221a6 | ||
|
|
b44373915a | ||
|
|
28cf3a17fa | ||
|
|
754d6b269d | ||
|
|
c2c2911c6e | ||
|
|
4a8e8cacf2 | ||
|
|
a863808947 | ||
|
|
1935bf8c3c | ||
|
|
55fe185c72 | ||
|
|
e11ceb9d20 | ||
|
|
6b851dfc12 | ||
|
|
b7fac6df3e | ||
|
|
63a2d3850a | ||
|
|
5322064e9e | ||
|
|
860679126a | ||
|
|
4ef67c0cf7 | ||
|
|
286ae4b1f5 | ||
|
|
7e43b2f768 | ||
|
|
ae48f50037 | ||
|
|
d6a02a3d4b | ||
|
|
1992002d2d | ||
|
|
ede5724069 | ||
|
|
1593d9644a | ||
|
|
8d0ed80e0c | ||
|
|
a3d1844c2a | ||
|
|
8313952a8e | ||
|
|
faf5515e4b | ||
|
|
921e8f93b7 | ||
|
|
422ff828cd | ||
|
|
2df8e1d5cd | ||
|
|
11050441e5 | ||
|
|
8bd659ba10 | ||
|
|
93d7076901 | ||
|
|
0fc89c3bb6 | ||
|
|
e2903319b7 | ||
|
|
489171a14c | ||
|
|
ddb19ab3da | ||
|
|
e90757081f | ||
|
|
4d3d5e794c | ||
|
|
7eec015ff1 | ||
|
|
0e2077095d | ||
|
|
52cf9b1e03 | ||
|
|
541e12a108 | ||
|
|
f8c89dd58a | ||
|
|
d0ccdbd68d | ||
|
|
edefe8abda | ||
|
|
ce0830812e | ||
|
|
4e2ae32874 | ||
|
|
e93a9c70c5 | ||
|
|
5ffa751cff | ||
|
|
9bf24d6dc5 | ||
|
|
7586006e7f | ||
|
|
6e4f7a887a | ||
|
|
60fc7d6a11 | ||
|
|
7ca9df1b2b | ||
|
|
2a2e96378a | ||
|
|
1f04d6c1be | ||
|
|
ab41183ce5 | ||
|
|
414b974505 | ||
|
|
684267b19b | ||
|
|
64a9f3828b | ||
|
|
0f428db983 | ||
|
|
1078df99aa | ||
|
|
76fb2f56d4 | ||
|
|
54edfdcb04 | ||
|
|
6ec04ca65e | ||
|
|
6c7f535676 | ||
|
|
3708917f15 | ||
|
|
6160718fd2 | ||
|
|
ee9593a7e0 | ||
|
|
581aab67a6 | ||
|
|
7cc6c1484a | ||
|
|
f87c6cd2aa | ||
|
|
3ee0aafebc | ||
|
|
ced674e48a | ||
|
|
b8e8bbe482 | ||
|
|
72a35ce905 | ||
|
|
a9e566ffee | ||
|
|
bca2022351 | ||
|
|
e4595fd234 | ||
|
|
0ea2c7fdde | ||
|
|
878a1f8af8 |
No files matched your search
+181
-24
@@ -2,11 +2,11 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@3.3.1
|
||||
cypress: cypress-io/cypress@3.4.2
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: cimg/node:18.18
|
||||
- image: cimg/node:20.18.1
|
||||
environment:
|
||||
TERM: xterm
|
||||
QUICK_BUILD: true
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build and push Docker image from the release branch
|
||||
name: Build Docker image for amd64
|
||||
command: |
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job.
|
||||
@@ -222,12 +222,50 @@ jobs:
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our image, auth, and push
|
||||
docker build --tag ohif/app:$IMAGE_VERSION_FULL --tag ohif/app:latest .
|
||||
# Build our amd64 image, auth, and push
|
||||
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-amd64 .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL
|
||||
docker push ohif/app:latest
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
|
||||
docker push ohif/app:latest-amd64
|
||||
fi
|
||||
- persist_to_workspace:
|
||||
root: ~/repo
|
||||
paths:
|
||||
- .
|
||||
|
||||
DOCKER_RELEASE_PUBLISH_ARM:
|
||||
<<: *defaults
|
||||
resource_class: arm.large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build Docker image for arm64 (Release)
|
||||
command: |
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job.
|
||||
if [[ ! -e version.txt ]]; then
|
||||
exit 0
|
||||
else
|
||||
# Remove npm config
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our arm64 image, auth, and push
|
||||
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-arm64 .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
|
||||
docker push ohif/app:latest-arm64
|
||||
fi
|
||||
- persist_to_workspace:
|
||||
root: ~/repo
|
||||
paths:
|
||||
- .
|
||||
|
||||
DOCKER_BETA_PUBLISH:
|
||||
<<: *defaults
|
||||
@@ -238,7 +276,7 @@ jobs:
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build and push Docker image from the master branch (beta releases)
|
||||
name: Build Docker image for amd64 (Beta)
|
||||
command: |
|
||||
echo $(ls -l)
|
||||
|
||||
@@ -250,19 +288,50 @@ jobs:
|
||||
else
|
||||
echo "Building and pushing Docker image from the master branch (beta releases)"
|
||||
rm -f ./.npmrc
|
||||
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our image, auth, and push
|
||||
|
||||
echo "starting docker build"
|
||||
docker build --tag ohif/app:$IMAGE_VERSION_FULL .
|
||||
# Build our amd64 image, auth, and push
|
||||
docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-beta-amd64 .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL-amd64
|
||||
docker push ohif/app:latest-beta-amd64
|
||||
fi
|
||||
|
||||
echo "starting docker push"
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL
|
||||
DOCKER_BETA_PUBLISH_ARM:
|
||||
<<: *defaults
|
||||
resource_class: arm.large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build Docker image for arm64 (Beta)
|
||||
command: |
|
||||
echo $(ls -l)
|
||||
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job.
|
||||
if [[ ! -e version.txt ]]; then
|
||||
echo "don't have version txt"
|
||||
exit 0
|
||||
else
|
||||
echo "Building and pushing ARM64 Docker image from the master branch (beta releases)"
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our arm64 image, auth, and push
|
||||
docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-beta-arm64 .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL-arm64
|
||||
docker push ohif/app:latest-beta-arm64
|
||||
fi
|
||||
|
||||
CYPRESS:
|
||||
@@ -270,10 +339,11 @@ jobs:
|
||||
resource_class: large
|
||||
parallelism: 8
|
||||
steps:
|
||||
- install_bun
|
||||
- run:
|
||||
name: Install Xvfb
|
||||
command: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
name: Install System Dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6
|
||||
- run:
|
||||
name: Start Xvfb
|
||||
command: Xvfb :99 -screen 0 1920x1080x24 &
|
||||
@@ -282,14 +352,89 @@ jobs:
|
||||
name: Export Display Variable
|
||||
command: export DISPLAY=:99
|
||||
- cypress/install:
|
||||
install-browsers: true
|
||||
install-command: bun install --no-save
|
||||
package-manager: yarn
|
||||
install-command: yarn install --no-save
|
||||
- cypress/run-tests:
|
||||
cypress-command:
|
||||
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record
|
||||
--browser chrome --parallel
|
||||
start-command: bun run test:data && bun run test:e2e:serve
|
||||
cypress-command: |
|
||||
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record --parallel
|
||||
start-command: yarn run test:data && yarn run test:e2e:serve
|
||||
|
||||
DOCKER_MULTIARCH_MANIFEST:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Create and push multi-architecture manifest (Release)
|
||||
command: |
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job.
|
||||
if [[ ! -e version.txt ]]; then
|
||||
exit 0
|
||||
else
|
||||
echo "Building and pushing multi-architecture manifest from the master branch (release releases)"
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
|
||||
# Create and push manifest for specific version
|
||||
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
|
||||
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
|
||||
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
|
||||
docker manifest push ohif/app:$IMAGE_VERSION_FULL
|
||||
|
||||
# Create and push manifest for "latest" tag
|
||||
docker manifest create ohif/app:latest \
|
||||
--amend ohif/app:latest-amd64 \
|
||||
--amend ohif/app:latest-arm64
|
||||
docker manifest push ohif/app:latest
|
||||
fi
|
||||
|
||||
DOCKER_BETA_MULTIARCH_MANIFEST:
|
||||
<<: *defaults
|
||||
resource_class: large
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Create and push multi-architecture manifest (Beta)
|
||||
command: |
|
||||
echo $(ls -l)
|
||||
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job.
|
||||
if [[ ! -e version.txt ]]; then
|
||||
exit 0
|
||||
else
|
||||
echo "Building and pushing multi-architecture manifest from the master branch (beta releases)"
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
|
||||
# Create and push manifest for specific beta version
|
||||
docker manifest create ohif/app:$IMAGE_VERSION_FULL \
|
||||
--amend ohif/app:$IMAGE_VERSION_FULL-amd64 \
|
||||
--amend ohif/app:$IMAGE_VERSION_FULL-arm64
|
||||
docker manifest push ohif/app:$IMAGE_VERSION_FULL
|
||||
|
||||
# Create and push manifest for "latest-beta" tag
|
||||
docker manifest create ohif/app:latest-beta \
|
||||
--amend ohif/app:latest-beta-amd64 \
|
||||
--amend ohif/app:latest-beta-arm64
|
||||
docker manifest push ohif/app:latest-beta
|
||||
fi
|
||||
|
||||
workflows:
|
||||
PR_CHECKS:
|
||||
@@ -316,6 +461,12 @@ workflows:
|
||||
- DOCKER_BETA_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
- DOCKER_BETA_PUBLISH_ARM:
|
||||
requires:
|
||||
- DOCKER_BETA_PUBLISH
|
||||
- DOCKER_BETA_MULTIARCH_MANIFEST:
|
||||
requires:
|
||||
- DOCKER_BETA_PUBLISH_ARM
|
||||
|
||||
# viewer.ohif.org
|
||||
DEPLOY_RELEASE:
|
||||
@@ -334,3 +485,9 @@ workflows:
|
||||
- DOCKER_RELEASE_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
- DOCKER_RELEASE_PUBLISH_ARM:
|
||||
requires:
|
||||
- DOCKER_RELEASE_PUBLISH
|
||||
- DOCKER_MULTIARCH_MANIFEST:
|
||||
requires:
|
||||
- DOCKER_RELEASE_PUBLISH_ARM
|
||||
@@ -1,10 +1,13 @@
|
||||
server {
|
||||
gzip_static always;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gunzip on;
|
||||
listen ${PORT} default_server;
|
||||
listen [::]:${PORT} default_server;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
try_files $uri $uri/ ${PUBLIC_URL}index.html;
|
||||
add_header Cross-Origin-Resource-Policy same-origin;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -2,16 +2,34 @@
|
||||
|
||||
if [ -n "$SSL_PORT" ]
|
||||
then
|
||||
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
|
||||
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template | envsubst '${PUBLIC_URL}' > /etc/nginx/conf.d/default.conf
|
||||
else
|
||||
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
envsubst '${PORT}:${PUBLIC_URL}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
if [ -n "$APP_CONFIG" ]
|
||||
then
|
||||
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
|
||||
if [ -n "$APP_CONFIG" ]; then
|
||||
echo "$APP_CONFIG" > /usr/share/nginx/html${PUBLIC_URL}app-config.js
|
||||
echo "Using custom APP_CONFIG environment variable"
|
||||
else
|
||||
echo "Not using custom APP_CONFIG"
|
||||
fi
|
||||
|
||||
if [ -f /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
|
||||
if [ -s /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then
|
||||
echo "Detected non-empty app-config.js. Ensuring .gz file is updated..."
|
||||
rm -f /usr/share/nginx/html${PUBLIC_URL}app-config.js.gz
|
||||
gzip /usr/share/nginx/html${PUBLIC_URL}app-config.js
|
||||
touch /usr/share/nginx/html${PUBLIC_URL}app-config.js
|
||||
echo "Compressed app-config.js to app-config.js.gz"
|
||||
else
|
||||
echo "app-config.js is empty. Skipping compression."
|
||||
fi
|
||||
else
|
||||
echo "No app-config.js file found. Skipping compression."
|
||||
fi
|
||||
|
||||
|
||||
|
||||
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
then
|
||||
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
|
||||
@@ -40,6 +58,6 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||
fi
|
||||
|
||||
echo "Starting Nginx to serve the OHIF Viewer..."
|
||||
echo "Starting Nginx to serve the OHIF Viewer on ${PUBLIC_URL}"
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,4 @@
|
||||
find platform/app/dist -name "*.js" -exec gzip -9 "{}" \; -exec touch "{}" \;
|
||||
find platform/app/dist -name "*.map" -exec gzip -9 "{}" \; -exec touch "{}" \;
|
||||
find platform/app/dist -name "*.css" -exec gzip -9 "{}" \; -exec touch "{}" \;
|
||||
find platform/app/dist -name "*.svg" -exec gzip -9 "{}" \; -exec touch "{}" \;
|
||||
+4
-4
@@ -8,11 +8,11 @@
|
||||
# have this file as their .dockerignore.
|
||||
|
||||
# Output
|
||||
dist/
|
||||
build/
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Root
|
||||
README.md
|
||||
@@ -33,5 +33,5 @@ dockerfile
|
||||
.scripts/
|
||||
.vscode/
|
||||
coverage/
|
||||
docs/
|
||||
platform/docs/
|
||||
testdata/
|
||||
+2
-1
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"rules": {
|
||||
// Enforce consistent brace style for all control statements for readability
|
||||
"curly": "error"
|
||||
"curly": "error",
|
||||
"import/no-anonymous-default-export": "off"
|
||||
},
|
||||
"globals": {
|
||||
"cy": true,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
- package-ecosystem: 'bun'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
labels: ['dependencies']
|
||||
commit-message:
|
||||
prefix: 'chore'
|
||||
include: 'scope'
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
labels: ['dependencies']
|
||||
commit-message:
|
||||
prefix: 'chore'
|
||||
include: 'scope'
|
||||
@@ -64,7 +64,7 @@ body:
|
||||
attributes:
|
||||
label: 'Node version'
|
||||
description: 'Your Node.js version.'
|
||||
placeholder: 'e.g., 18.16.1'
|
||||
placeholder: 'e.g., 20.18.1'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
name: Build and Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy-docs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
# Need permissions to read actions and pull requests
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20 # Or your desired Node version
|
||||
|
||||
- name: Install root dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
# Removed Playwright tests and coverage generation steps
|
||||
|
||||
- name: Find PR and associated workflow run
|
||||
id: find_pr_run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MERGE_COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
# Find the PR associated with the merge commit SHA
|
||||
# Note: This relies on the merge commit being directly pushed to main
|
||||
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
|
||||
if [ -z "$PR_DATA" ]; then
|
||||
echo "Could not find merged PR for commit $MERGE_COMMIT_SHA."
|
||||
# Decide how to handle - fail, or maybe generate coverage now?
|
||||
# For now, let's fail.
|
||||
exit 1
|
||||
fi
|
||||
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
|
||||
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
|
||||
echo "Found PR Number: $PR_NUMBER"
|
||||
echo "Found PR Head SHA: $PR_HEAD_SHA"
|
||||
|
||||
# Find the latest workflow run ID for the playwright workflow on the PR head commit
|
||||
# Uses the workflow file name 'playwright.yml'
|
||||
# Remove the --status success flag to find any run
|
||||
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --json databaseId --jq '.[0].databaseId')
|
||||
|
||||
if [ -z "$RUN_ID" ]; then
|
||||
echo "Could not find any 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA)."
|
||||
# Decide how to handle - maybe try finding the artifact from the merge commit run if that exists?
|
||||
# For now, let's fail.
|
||||
exit 1
|
||||
fi
|
||||
echo "Found Run ID: $RUN_ID"
|
||||
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download coverage artifact from PR run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p ./coverage-artifact
|
||||
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
|
||||
# Check if download was successful (e.g., check if files exist)
|
||||
if [ ! -f ./coverage-artifact/base.css ]; then
|
||||
echo "Failed to download or find expected files in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
|
||||
exit 1
|
||||
fi
|
||||
echo "Artifact downloaded successfully."
|
||||
|
||||
- name: Install docs dependencies
|
||||
run: cd platform/docs && bun install
|
||||
|
||||
- name: Copy coverage to docs static directory
|
||||
run: |
|
||||
# Copy files from the downloaded artifact directory
|
||||
mkdir -p platform/docs/static/coverage
|
||||
cp -r ./coverage-artifact/* platform/docs/static/coverage/
|
||||
# Copy specific asset files from the downloaded artifact root to static root
|
||||
cp ./coverage-artifact/base.css platform/docs/static/
|
||||
cp ./coverage-artifact/block-navigation.js platform/docs/static/
|
||||
cp ./coverage-artifact/prettify.css platform/docs/static/
|
||||
cp ./coverage-artifact/prettify.js platform/docs/static/
|
||||
cp ./coverage-artifact/favicon.png platform/docs/static/
|
||||
cp ./coverage-artifact/sort-arrow-sprite.png platform/docs/static/
|
||||
cp ./coverage-artifact/sorter.js platform/docs/static/
|
||||
|
||||
- name: Build docs
|
||||
run: cd platform/docs && bun run build
|
||||
|
||||
- name: Deploy to Netlify
|
||||
run: |
|
||||
cd platform/docs
|
||||
npx netlify-cli deploy --dir=./build --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
@@ -0,0 +1,98 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '15 1 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -1,76 +1,54 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
playwright-tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.49.0-noble
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
shardTotal: [10]
|
||||
node-version: [20]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install unzip
|
||||
run: apt-get update && apt-get install -y unzip
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Set up Git safe directory
|
||||
run: git config --global --add safe.directory /__w/Viewers/Viewers
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install
|
||||
- name: Run Playwright tests
|
||||
run:
|
||||
export NODE_OPTIONS="--max_old_space_size=8192" && bun x playwright test --shard=${{
|
||||
matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=10192"
|
||||
bun run test:e2e:coverage
|
||||
- name: Create directory of test results
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
mkdir -p packaged-test-results
|
||||
cp -r ./tests/test-results packaged-test-results/ || true
|
||||
cp ./tests/playwright-report.json packaged-test-results/ || true
|
||||
- name: Upload directory of test results artifact
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [playwright-tests]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||
steps:
|
||||
- name: Install unzip
|
||||
run: apt-get update && apt-get install -y unzip
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Set up Git safe directory
|
||||
run: git config --global --add safe.directory /__w/Viewers/Viewers
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: bun x playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
name: playwright-results
|
||||
path: packaged-test-results/
|
||||
retention-days: 5
|
||||
- name: create the coverage report
|
||||
run: |
|
||||
bun nyc report --reporter=lcov --reporter=text
|
||||
- name: Upload the coverage report to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
name: coverage-report-pr
|
||||
path: coverage
|
||||
retention-days: 3
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
# Packages
|
||||
node_modules
|
||||
|
||||
.cursor/
|
||||
.nyc_output/
|
||||
# Output
|
||||
build
|
||||
dist
|
||||
@@ -12,6 +13,7 @@ coverage/
|
||||
.yarn/
|
||||
.nx/
|
||||
addOns/yarn.lock
|
||||
playwright-report/
|
||||
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
@@ -56,3 +58,8 @@ tests/test-results/
|
||||
tests/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
# Backup files
|
||||
*~
|
||||
+1
-1
@@ -1 +1 @@
|
||||
18.16.1
|
||||
20.9.0
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"instrument": true,
|
||||
"sourceMap": true,
|
||||
"cache": false,
|
||||
"all": true,
|
||||
"include": [
|
||||
"platform/*/src/**/*.ts",
|
||||
"platform/*/src/**/*.js",
|
||||
"extensions/*/src/**/*.ts",
|
||||
"extensions/*/src/**/*.js",
|
||||
"modes/*/src/**/*.ts",
|
||||
"modes/*/src/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/examples/**",
|
||||
"**/stories/**",
|
||||
"platform/docs/**"
|
||||
]
|
||||
}
|
||||
Vendored
+4
-1
@@ -82,6 +82,7 @@
|
||||
"Reslices",
|
||||
"roadmap",
|
||||
"ROADMAPS",
|
||||
"rtstruct",
|
||||
"Segmentations",
|
||||
"semibold",
|
||||
"sitk",
|
||||
@@ -109,6 +110,8 @@
|
||||
"wado",
|
||||
"wadors",
|
||||
"wadouri",
|
||||
"workerpool"
|
||||
"workerpool",
|
||||
"Colorbar",
|
||||
"Colorbars"
|
||||
]
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
const QUICK_BUILD = process.env.QUICK_BUILD;
|
||||
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
|
||||
const IS_COVERAGE = process.env.COVERAGE === 'true';
|
||||
|
||||
// read from ../version.txt
|
||||
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
|
||||
@@ -49,6 +50,7 @@ const defineValues = {
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
|
||||
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
|
||||
'process.env.TEST_ENV': JSON.stringify(process.env.TEST_ENV || ''),
|
||||
};
|
||||
|
||||
// Only redefine updated values. This avoids warning messages in the logs
|
||||
@@ -99,14 +101,30 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
...(isProdBuild
|
||||
? []
|
||||
: [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: isProdBuild ? [] : ['react-refresh/babel'],
|
||||
},
|
||||
},
|
||||
...(IS_COVERAGE
|
||||
? [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||
plugins: ['istanbul'],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: isProdBuild ? [] : ['react-refresh/babel'],
|
||||
},
|
||||
},
|
||||
]),
|
||||
]),
|
||||
{
|
||||
test: /\.svg?$/,
|
||||
@@ -168,6 +186,10 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
], //.concat(vtkRules),
|
||||
},
|
||||
resolve: {
|
||||
|
||||
+2781
File diff suppressed because it is too large.
Load diff
+56
-31
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
|
||||
#
|
||||
# It's a good example of how to build our static application and package it
|
||||
@@ -19,61 +20,85 @@
|
||||
#
|
||||
|
||||
|
||||
# syntax=docker/dockerfile:1.7-labs
|
||||
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
|
||||
#
|
||||
# It's a good example of how to build our static application and package it
|
||||
# with a web server capable of hosting it as static content.
|
||||
#
|
||||
# docker build
|
||||
# --------------
|
||||
# If you would like to use this dockerfile to build and tag an image, make sure
|
||||
# you set the context to the project's root directory:
|
||||
# https://docs.docker.com/engine/reference/commandline/build/
|
||||
#
|
||||
#
|
||||
# SUMMARY
|
||||
# --------------
|
||||
# This dockerfile is used as an input for a second stage to make things run faster.
|
||||
#
|
||||
|
||||
|
||||
# Stage 1: Build the application
|
||||
# docker build -t ohif/viewer:latest .
|
||||
FROM node:18.16.1-slim as json-copier
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY ["package.json", "yarn.lock", "preinstall.js", "./"]
|
||||
COPY extensions /usr/src/app/extensions
|
||||
COPY modes /usr/src/app/modes
|
||||
COPY platform /usr/src/app/platform
|
||||
|
||||
# Find and remove non-package.json files
|
||||
#RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
#RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
|
||||
# Copy Files
|
||||
FROM node:18.16.1-slim as builder
|
||||
FROM node:20.18.1-slim as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential python3
|
||||
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
RUN npm install -g bun
|
||||
# RUN npm install -g lerna@7.4.2
|
||||
ENV PATH=/usr/src/app/node_modules/.bin:$PATH
|
||||
|
||||
COPY --from=json-copier /usr/src/app .
|
||||
|
||||
# Do an initial install and then a final install
|
||||
COPY package.json yarn.lock preinstall.js lerna.json ./
|
||||
COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
|
||||
# Run the install before copying the rest of the files
|
||||
RUN yarn config set workspaces-experimental true
|
||||
RUN yarn install --frozen-lockfile --verbose
|
||||
|
||||
COPY . .
|
||||
RUN bun pm cache rm
|
||||
RUN bun install
|
||||
# Copy the local directory
|
||||
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . .
|
||||
|
||||
# To restore workspaces symlinks
|
||||
RUN yarn install --frozen-lockfile --verbose
|
||||
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
# Build here
|
||||
# After install it should hopefully be stable until the local directory changes
|
||||
ENV QUICK_BUILD true
|
||||
# ENV GENERATE_SOURCEMAP=false
|
||||
# ENV REACT_APP_CONFIG=config/default.js
|
||||
ARG APP_CONFIG=config/default.js
|
||||
ARG PUBLIC_URL=/
|
||||
ENV PUBLIC_URL=${PUBLIC_URL}
|
||||
|
||||
RUN yarn run build
|
||||
RUN bun run show:config
|
||||
RUN bun run build
|
||||
|
||||
# Precompress files
|
||||
RUN chmod u+x .docker/compressDist.sh
|
||||
RUN ./.docker/compressDist.sh
|
||||
|
||||
# Stage 3: Bundle the built application into a Docker container
|
||||
# which runs Nginx using Alpine Linux
|
||||
FROM nginxinc/nginx-unprivileged:1.25-alpine as final
|
||||
FROM nginxinc/nginx-unprivileged:1.27-alpine as final
|
||||
#RUN apk add --no-cache bash
|
||||
ENV PORT=80
|
||||
ARG PUBLIC_URL=/
|
||||
ENV PUBLIC_URL=${PUBLIC_URL}
|
||||
ARG PORT=80
|
||||
ENV PORT=${PORT}
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
USER nginx
|
||||
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
|
||||
RUN chmod 777 /usr/src/entrypoint.sh
|
||||
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html${PUBLIC_URL}
|
||||
# Copy paths that are renamed/redirected generally
|
||||
# Microscopy libraries depend on root level include, so must be copied
|
||||
COPY --from=builder /usr/src/app/platform/app/dist/dicom-microscopy-viewer /usr/share/nginx/html/dicom-microscopy-viewer
|
||||
|
||||
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
|
||||
# The nginx user cannot chmod it, so change to root.
|
||||
USER root
|
||||
RUN chmod 666 /usr/share/nginx/html/app-config.js
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html
|
||||
USER nginx
|
||||
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,5 +1,4 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<div align="center">
|
||||
<h1>OHIF Medical Imaging Viewer</h1>
|
||||
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
|
||||
@@ -135,7 +134,7 @@ Here is a schematic representation of our development workflow:
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
||||
- [Yarn 1.20.0+](https://yarnpkg.com/en/docs/install)
|
||||
- [Node 18+](https://nodejs.org/en/)
|
||||
- Yarn Workspaces should be enabled on your machine:
|
||||
- `yarn config set workspaces-experimental true`
|
||||
@@ -171,7 +170,8 @@ also supports a number of commands that can be found in their respective
|
||||
| Yarn Commands | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------- |
|
||||
| **Develop** | |
|
||||
| `dev` or `start` | Default development experience for Viewer |
|
||||
| `dev` | Default development experience for Viewer |
|
||||
| `dev:fast` | Our experimental fast dev mode that uses rsbuild instead of webpack |
|
||||
| `test:unit` | Jest multi-project test runner; overall coverage |
|
||||
| **Deploy** | |
|
||||
| `build`\* | Builds production output for our PWA Viewer | |
|
||||
|
||||
+2144
File diff suppressed because it is too large.
Load diff
+16
-15
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@externals/devDependencies",
|
||||
"description": "External dev dependencies - put dev build dependencies here",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@@ -10,23 +10,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"core-js": "^3.2.1",
|
||||
"moment": "^2.9.4"
|
||||
"moment": "^2.30.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.28.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@rsbuild/core": "^1.3.16",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.3.0",
|
||||
"@rsbuild/plugin-react": "^1.3.1",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/helpers": "^0.5.15",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"babel-eslint": "9.x",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
@@ -35,14 +39,14 @@
|
||||
"css-loader": "^6.8.1",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-flowtype": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-prettier": "5.5.1",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
@@ -64,25 +68,22 @@
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-loader": "^6.1.1",
|
||||
"postcss-preset-env": "^7.4.3",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-tailwindcss": "0.6.9",
|
||||
"react-refresh": "^0.14.2",
|
||||
"semver": "^7.5.1",
|
||||
"serve": "^14.2.0",
|
||||
"serve": "^14.2.4",
|
||||
"shader-loader": "^1.3.1",
|
||||
"shx": "^0.3.3",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"start-server-and-test": "^1.10.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"stylus": "^0.59.0",
|
||||
"stylus-loader": "^7.1.3",
|
||||
"terser-webpack-plugin": "^5.1.4",
|
||||
"typescript": "5.5.4",
|
||||
"unused-webpack-plugin": "2.4.0",
|
||||
"webpack": "5.94.0",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "4.7.3",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "5.2.2",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"workbox-webpack-plugin": "^6.1.5",
|
||||
|
||||
+2120
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@externals/dicom-microscopy-viewer",
|
||||
"description": "External reference to dicom-microscopy-viewer",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dicom-microscopy-viewer": "^0.46.1"
|
||||
|
||||
+11
-11
@@ -21,24 +21,24 @@
|
||||
"preinstall": "cd .. && node preinstall.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/plugin-transform-class-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
|
||||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.16.7",
|
||||
"@babel/plugin-transform-regenerator": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/plugin-transform-typescript": "^7.13.0",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.13.0"
|
||||
"@babel/plugin-transform-runtime": "7.28.0",
|
||||
"@babel/plugin-transform-typescript": "^7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.27.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@babel/runtime": "^7.20.13",
|
||||
"commander": "8.3.0",
|
||||
"dcmjs": ">=0.33.0",
|
||||
"dcmjs": "0.43.1",
|
||||
"dicomweb-client": ">=0.10.4",
|
||||
"nth-check": "^2.1.1",
|
||||
"trim-newlines": "^5.0.0",
|
||||
|
||||
+8
-7
@@ -1,14 +1,12 @@
|
||||
// https://babeljs.io/docs/en/options#babelrcroots
|
||||
const { extendDefaultPlugins } = require('svgo');
|
||||
|
||||
module.exports = {
|
||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
['@babel/plugin-transform-class-properties', { loose: true }],
|
||||
'@babel/plugin-transform-typescript',
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
['@babel/plugin-transform-private-property-in-object', { loose: true }],
|
||||
['@babel/plugin-transform-private-methods', { loose: true }],
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
],
|
||||
env: {
|
||||
@@ -20,19 +18,23 @@ module.exports = {
|
||||
{
|
||||
modules: 'commonjs',
|
||||
debug: false,
|
||||
targets: { node: 'current' },
|
||||
bugfixes: true,
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'babel-plugin-istanbul',
|
||||
'@babel/plugin-transform-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/transform-destructuring',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
'@babel/plugin-transform-for-of',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
@@ -51,7 +53,6 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
085590a4ca64bebad9ef60503411e1a6dd4d93f9
|
||||
9d94e26d5d15ccd19f12020dbc5438364d88d253
|
||||
@@ -0,0 +1,118 @@
|
||||
diff --git a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
|
||||
index 6c1e4f8f6..56dac15b5 100644
|
||||
--- a/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
|
||||
+++ b/extensions/cornerstone/src/components/ViewportOrientationMenu/ViewportOrientationMenu.tsx
|
||||
@@ -156,65 +156,65 @@ function ViewportOrientationMenu({
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
|
||||
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
|
||||
onClick={() => handleOrientationChange('axial')}
|
||||
>
|
||||
- <span className="mr-0 flex w-4 items-center justify-center">
|
||||
+ <div className="mr-1 flex w-6 items-center justify-start">
|
||||
{currentOrientation === 'axial' ? (
|
||||
- <Icons.Checked className="text-primary h-5 w-5" />
|
||||
+ <Icons.Checked className="text-primary h-6 w-6" />
|
||||
) : null}
|
||||
- </span>
|
||||
- <span className="flex-1 text-left">Axial</span>
|
||||
+ </div>
|
||||
+ <div className="flex-1 text-left">Axial</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
|
||||
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
|
||||
onClick={() => handleOrientationChange('sagittal')}
|
||||
>
|
||||
- <span className="mr-0 flex w-4 items-center justify-center">
|
||||
+ <div className="mr-1 flex w-6 items-center justify-start">
|
||||
{currentOrientation === 'sagittal' ? (
|
||||
- <Icons.Checked className="text-primary h-5 w-5" />
|
||||
+ <Icons.Checked className="text-primary h-6 w-6" />
|
||||
) : null}
|
||||
- </span>
|
||||
- <span className="flex-1 text-left">Sagittal</span>
|
||||
+ </div>
|
||||
+ <div className="flex-1 text-left">Sagittal</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
|
||||
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
|
||||
onClick={() => handleOrientationChange('coronal')}
|
||||
>
|
||||
- <span className="mr-0 flex w-4 items-center justify-center">
|
||||
+ <div className="mr-1 flex w-6 items-center justify-start">
|
||||
{currentOrientation === 'coronal' ? (
|
||||
- <Icons.Checked className="text-primary h-5 w-5" />
|
||||
+ <Icons.Checked className="text-primary h-6 w-6" />
|
||||
) : null}
|
||||
- </span>
|
||||
- <span className="flex-1 text-left">Coronal</span>
|
||||
+ </div>
|
||||
+ <div className="flex-1 text-left">Coronal</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
|
||||
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
|
||||
onClick={() => handleOrientationChange('acquisition')}
|
||||
>
|
||||
- <span className="mr-0 flex w-4 items-center justify-center">
|
||||
+ <div className="mr-1 flex w-6 items-center justify-start">
|
||||
{currentOrientation === 'acquisition' ? (
|
||||
- <Icons.Checked className="text-primary h-5 w-5" />
|
||||
+ <Icons.Checked className="text-primary h-6 w-6" />
|
||||
) : null}
|
||||
- </span>
|
||||
- <span className="flex-1 text-left">Acquisition</span>
|
||||
+ </div>
|
||||
+ <div className="flex-1 text-left">Acquisition</div>
|
||||
</Button>
|
||||
{/* Divider */}
|
||||
- <div className="my-2 border-t border-white/20" />
|
||||
+ <div className="mx-1 my-2 border-t border-white/20" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
- className="flex h-[28px] flex-shrink-0 items-center justify-start self-stretch"
|
||||
+ className="flex h-7 w-full flex-shrink-0 items-center justify-start self-stretch px-1 py-0"
|
||||
onClick={() => handleOrientationChange('reformat')}
|
||||
>
|
||||
- <span className="mr-0 flex w-4 items-center justify-center">
|
||||
+ <div className="mr-1 flex w-6 items-center justify-start">
|
||||
{currentOrientation === 'reformat' ? (
|
||||
- <Icons.Checked className="text-primary h-5 w-5" />
|
||||
+ <Icons.Checked className="text-primary h-6 w-6" />
|
||||
) : null}
|
||||
- </span>
|
||||
- <span className="flex-1 text-left">Reformat</span>
|
||||
+ </div>
|
||||
+ <div className="flex-1 text-left">Reformat</div>
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
diff --git a/platform/ui-next/src/components/Icons/Sources/Checked.tsx b/platform/ui-next/src/components/Icons/Sources/Checked.tsx
|
||||
index 83c8fc57e..c88b75e0d 100644
|
||||
--- a/platform/ui-next/src/components/Icons/Sources/Checked.tsx
|
||||
+++ b/platform/ui-next/src/components/Icons/Sources/Checked.tsx
|
||||
@@ -4,14 +4,14 @@ import type { IconProps } from '../types';
|
||||
export const Checked = (props: IconProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
- width="24"
|
||||
+ width="20"
|
||||
height="24"
|
||||
- viewBox="0 0 24 24"
|
||||
+ viewBox="0 0 20 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
- d="M16 9L11.3333 15L8 12.4737"
|
||||
+ d="M14 9L9.3333 15L6 12.4737"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-transform-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-transform-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "DICOM Parametric Map read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.12",
|
||||
"@ohif/extension-default": "3.10.0-beta.12",
|
||||
"@ohif/i18n": "3.10.0-beta.12",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/extension-cornerstone": "3.11.0",
|
||||
"@ohif/extension-default": "3.11.0",
|
||||
"@ohif/i18n": "3.11.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.2.20",
|
||||
"@cornerstonejs/core": "^2.2.20",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"@cornerstonejs/adapters": "^3.32.5",
|
||||
"@cornerstonejs/core": "^3.32.5",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
||||
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomLoaderService } from '@ohif/extension-cornerstone';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
import { dicomLoaderService, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone';
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
@@ -57,7 +56,8 @@ function _getDisplaySetsFromSeries(
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
supportsWindowLevel: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@@ -227,7 +227,8 @@ async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
|
||||
return derivedVolume;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const {
|
||||
displaySets,
|
||||
children,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
const { displaySets, children, viewportOptions, displaySetOptions, servicesManager } = props;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const { displaySetService, segmentationService, uiNotificationService } =
|
||||
const { displaySetService, segmentationService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
// PMAP viewport will always have a single display set
|
||||
@@ -20,6 +14,10 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
throw new Error('PMAP viewport must have a single display set');
|
||||
}
|
||||
|
||||
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
|
||||
'ui.loadingIndicatorTotalPercent'
|
||||
);
|
||||
|
||||
const pmapDisplaySet = displaySets[0];
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
@@ -55,9 +53,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
displaySetOptions.unshift({});
|
||||
const [pmapDisplaySetOptions] = displaySetOptions;
|
||||
@@ -89,7 +84,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||
@@ -97,17 +92,19 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||
></Component>
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
extensionManager,
|
||||
displaySetOptions,
|
||||
props,
|
||||
pmapDisplaySet,
|
||||
viewportOptions.orientation,
|
||||
viewportOptions.viewportId,
|
||||
viewportOptions.presentationIds,
|
||||
uiNotificationService,
|
||||
]);
|
||||
|
||||
// Cleanup the PMAP viewport when the viewport is destroyed
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-transform-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-transform-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
@@ -37,7 +37,6 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.12",
|
||||
"@ohif/extension-default": "3.10.0-beta.12",
|
||||
"@ohif/i18n": "3.10.0-beta.12",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/extension-cornerstone": "3.11.0",
|
||||
"@ohif/extension-default": "3.11.0",
|
||||
"@ohif/i18n": "3.11.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -27,7 +27,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
// it will be in the correct position zoom and pan
|
||||
commandsManager.runCommand('updateStoredPositionPresentation', {
|
||||
viewportId,
|
||||
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
|
||||
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
|
||||
});
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import loadRTStruct from './loadRTStruct';
|
||||
@@ -30,7 +31,7 @@ function _getDisplaySetsFromSeries(
|
||||
const displaySet = {
|
||||
Modality: 'RTSTRUCT',
|
||||
loading: false,
|
||||
isReconstructable: false, // by default for now since it is a volumetric SEG currently
|
||||
isReconstructable: false,
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
@@ -53,6 +54,7 @@ function _getDisplaySetsFromSeries(
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('RTSTRUCT')}`,
|
||||
};
|
||||
|
||||
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@@ -85,6 +87,7 @@ function _getDisplaySetsFromSeries(
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -92,14 +95,22 @@ function _getDisplaySetsFromSeries(
|
||||
} else {
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
|
||||
}
|
||||
|
||||
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
||||
displaySet.load = ({ headers, createSegmentation = true }) =>
|
||||
_load(displaySet, servicesManager, extensionManager, headers, createSegmentation);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
function _load(
|
||||
rtDisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers,
|
||||
createSegmentation = true
|
||||
) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@@ -121,16 +132,21 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
|
||||
rtDisplaySet.structureSet = structureSet;
|
||||
}
|
||||
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
rtDisplaySet.loading = false;
|
||||
reject(error);
|
||||
});
|
||||
if (createSegmentation) {
|
||||
segmentationService
|
||||
.createSegmentationForRTDisplaySet(rtDisplaySet)
|
||||
.then(() => {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
rtDisplaySet.loading = false;
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
rtDisplaySet.loading = false;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
return loadPromises[SOPInstanceUID];
|
||||
@@ -171,20 +187,14 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(
|
||||
rtDisplaySet,
|
||||
segmentationService: AppTypes.SegmentationService
|
||||
) {
|
||||
function _segmentationExistsInCache() {
|
||||
// Todo: fix this
|
||||
return false;
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
const rtContourId = rtDisplaySet.displaySetInstanceUID;
|
||||
const contour = segmentationService.getContour(rtContourId);
|
||||
|
||||
return contour !== undefined;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dicom-rt',
|
||||
|
||||
@@ -126,6 +126,7 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
ROIContours: [],
|
||||
visible: true,
|
||||
ReferencedSOPInstanceUIDsSet: new Set(),
|
||||
};
|
||||
|
||||
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||
@@ -142,7 +143,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
|
||||
const contourPoints = [];
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
|
||||
const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } =
|
||||
ContourSequenceArray[c];
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
@@ -172,6 +174,12 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet,
|
||||
type: ContourGeometricType,
|
||||
isSupported,
|
||||
});
|
||||
|
||||
if (ContourImageSequence?.ReferencedSOPInstanceUID) {
|
||||
structureSet.ReferencedSOPInstanceUIDsSet.add(
|
||||
ContourImageSequence?.ReferencedSOPInstanceUID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_setROIContourMetadata(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
HYDRATE_SEG: 5,
|
||||
};
|
||||
import { utils, Types } from '@ohif/extension-cornerstone';
|
||||
|
||||
function promptHydrateRT({
|
||||
servicesManager,
|
||||
@@ -12,70 +6,20 @@ function promptHydrateRT({
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateRTDisplaySet,
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
const extensionManager = servicesManager._extensionManager;
|
||||
const appConfig = extensionManager._appConfig;
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = appConfig?.disableConfirmationPrompts
|
||||
? RESPONSE.HYDRATE_SEG
|
||||
: await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
id: 'promptHydrateRT',
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
rtDisplaySet: AppTypes.DisplaySet;
|
||||
viewportId: string;
|
||||
preHydrateCallbacks?: Types.HydrationCallback[];
|
||||
hydrateRTDisplaySet: Types.HydrationCallback;
|
||||
}) {
|
||||
return utils.promptHydrationDialog({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
displaySet: rtDisplaySet,
|
||||
preHydrateCallbacks,
|
||||
hydrateCallback: hydrateRTDisplaySet,
|
||||
type: 'RTSTRUCT',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import {
|
||||
utils,
|
||||
usePositionPresentationStore,
|
||||
OHIFCornerstoneViewport,
|
||||
} from '@ohif/extension-cornerstone';
|
||||
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { children, displaySets, viewportOptions } = props as {
|
||||
children: React.ReactNode;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
viewportOptions: AppTypes.ViewportOptions;
|
||||
};
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
const { displaySetService, toolGroupService, segmentationService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
@@ -38,33 +32,40 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
throw new Error('RT viewport should only have a single display set');
|
||||
}
|
||||
|
||||
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
|
||||
'ui.loadingIndicatorTotalPercent'
|
||||
);
|
||||
|
||||
const rtDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [{ viewports, activeViewportId }, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
|
||||
// Hydration means that the RT is opened and segments are loaded into the
|
||||
// segmentation panel, and RT is also rendered on any viewport that is in the
|
||||
// same frameOfReferenceUID as the referencedSeriesUID of the RT. However,
|
||||
// loading basically means RT loading over network and bit unpacking of the
|
||||
// RT data.
|
||||
const [isHydrated, setIsHydrated] = useState(rtDisplaySet.isHydrated);
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
const [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded);
|
||||
const [element, setElement] = useState(null);
|
||||
const [processingProgress, setProcessingProgress] = useState({
|
||||
percentComplete: null,
|
||||
totalSegments: null,
|
||||
});
|
||||
|
||||
// refs
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||
// If the referencedDisplaySetInstanceUID is not found, it means the RTStruct series is being
|
||||
// launched without its corresponding referenced display set (e.g., the RTStruct series is launched using
|
||||
// series launch /mode?StudyInstanceUIDs=&SeriesInstanceUID).
|
||||
// In such cases, we attempt to handle this scenario gracefully by
|
||||
// invoking a custom handler. Ideally, if a user tries to launch a series that isn't viewable,
|
||||
// (eg.: we can prompt them with an explanation and provide a link to the full study).
|
||||
if (!referencedDisplaySetInstanceUID) {
|
||||
const missingReferenceDisplaySetHandler = customizationService.getCustomization(
|
||||
'missingReferenceDisplaySetHandler'
|
||||
);
|
||||
const { handled } = missingReferenceDisplaySetHandler();
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
@@ -74,110 +75,31 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
displaySet: referencedDisplaySet,
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
/**
|
||||
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||
* has enabled the element. Note: we delegate all the image rendering to
|
||||
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||
* the image rendering, element enabling etc.
|
||||
*/
|
||||
const onElementEnabled = evt => {
|
||||
setElement(evt.detail.element);
|
||||
};
|
||||
|
||||
const onElementDisabled = () => {
|
||||
setElement(null);
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
const hydrateRTDisplaySet = useCallback(
|
||||
({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('hydrateRTSDisplaySet', {
|
||||
displaySet: rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'stack',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportId, rtDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { segments } = segmentation;
|
||||
|
||||
const numberOfSegments = Object.keys(segments).length;
|
||||
|
||||
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||
|
||||
// Segment 0 is always background
|
||||
if (newSelectedSegmentIndex >= numberOfSegments - 1) {
|
||||
newSelectedSegmentIndex = 1;
|
||||
} else if (newSelectedSegmentIndex === 0) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment, segmentationService]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (rtIsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if not active viewport, return
|
||||
if (viewportId !== activeViewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
promptHydrateRT({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
rtDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateRTDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
hydrateRTDisplaySet: async () => {
|
||||
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
|
||||
displaySet: rtDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading, commandsManager, activeViewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
// I'm not sure what is this, since in RT we support Overlapping segments
|
||||
// via contours
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
@@ -185,11 +107,16 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
setRtIsLoading(false);
|
||||
}
|
||||
|
||||
if (evt.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message: 'Overlapping segments detected which is not currently supported',
|
||||
type: 'warning',
|
||||
if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
|
||||
const { firstSegmentedSliceImageId } = rtDisplaySet;
|
||||
const { presentationIds } = viewportOptions;
|
||||
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewportType: 'stack',
|
||||
viewReference: {
|
||||
referencedImageId: firstSegmentedSliceImageId,
|
||||
},
|
||||
viewPresentation: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -201,7 +128,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
}, [rtDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
const segmentLoadingSubscription = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||
({ percentComplete, numSegments }) => {
|
||||
setProcessingProgress({
|
||||
@@ -211,16 +138,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [rtDisplaySet]);
|
||||
|
||||
/**
|
||||
Cleanup the SEG viewport when the viewport is destroyed
|
||||
*/
|
||||
useEffect(() => {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
const displaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
@@ -234,9 +152,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
);
|
||||
|
||||
return () => {
|
||||
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||
segmentLoadingSubscription.unsubscribe();
|
||||
displaySetsRemovedSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [rtDisplaySet, displaySetService, viewports, activeViewportId, viewportGridService]);
|
||||
|
||||
useEffect(() => {
|
||||
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
@@ -247,43 +166,36 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
|
||||
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(rtDisplaySet.isHydrated);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentations(viewportId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
}, [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,
|
||||
});
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: viewportOptions.viewportType,
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
presentationIds: viewportOptions.presentationIds,
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [viewportId, rtDisplaySet, toolGroupId]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (
|
||||
@@ -306,43 +218,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">RTSTRUCT</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<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 (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-transform-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-transform-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
@@ -37,7 +37,6 @@ module.exports = {
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-refresh/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.12",
|
||||
"@ohif/extension-default": "3.10.0-beta.12",
|
||||
"@ohif/i18n": "3.10.0-beta.12",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/extension-cornerstone": "3.11.0",
|
||||
"@ohif/extension-default": "3.11.0",
|
||||
"@ohif/i18n": "3.11.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.2.20",
|
||||
"@cornerstonejs/core": "^2.2.20",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"@cornerstonejs/adapters": "^3.32.5",
|
||||
"@cornerstonejs/core": "^3.32.5",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { Types } from '@ohif/core';
|
||||
import { classes, Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
segmentation as cornerstoneToolsSegmentation,
|
||||
Enums as cornerstoneToolsEnums,
|
||||
utilities,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
@@ -45,13 +37,8 @@ const commandsModule = ({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const {
|
||||
segmentationService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
const { segmentationService, displaySetService, viewportGridService, toolGroupService } =
|
||||
servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
@@ -226,14 +213,6 @@ const commandsModule = ({
|
||||
* otherwise throws an error.
|
||||
*/
|
||||
storeSegmentation: async ({ segmentationId, dataSource }) => {
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action !== 1 && !promptResult.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
if (!segmentation) {
|
||||
@@ -241,33 +220,55 @@ const commandsModule = ({
|
||||
}
|
||||
|
||||
const { label } = segmentation;
|
||||
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
|
||||
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0];
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription,
|
||||
},
|
||||
const {
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
action,
|
||||
} = await createReportDialogPrompt({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
title: 'Store Segmentation',
|
||||
});
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
try {
|
||||
const selectedDataSourceConfig = selectedDataSource
|
||||
? extensionManager.getDataSources(selectedDataSource)[0]
|
||||
: defaultDataSource;
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription: reportName || label || 'Research Derived Series',
|
||||
},
|
||||
});
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
}
|
||||
|
||||
const { dataset: naturalizedReport } = generatedData;
|
||||
|
||||
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out
|
||||
if (naturalizedReport.StudyID === 'No Study ID') {
|
||||
naturalizedReport.StudyID = '';
|
||||
}
|
||||
|
||||
await selectedDataSourceConfig.store.dicom(naturalizedReport);
|
||||
|
||||
// add the information for where we stored it to the instance as well
|
||||
naturalizedReport.wadoRoot = selectedDataSourceConfig.getConfig().wadoRoot;
|
||||
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
return naturalizedReport;
|
||||
} catch (error) {
|
||||
console.debug('Error storing segmentation:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const { dataset: naturalizedReport } = generatedData;
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
|
||||
// add the information for where we stored it to the instance as well
|
||||
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
|
||||
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
return naturalizedReport;
|
||||
},
|
||||
/**
|
||||
* Converts segmentations into RTSS for download.
|
||||
@@ -276,21 +277,24 @@ const commandsModule = ({
|
||||
* converts dataset to downloadable blob.
|
||||
*
|
||||
*/
|
||||
downloadRTSS: ({ segmentationId }) => {
|
||||
downloadRTSS: async ({ segmentationId }) => {
|
||||
const segmentations = segmentationService.getSegmentation(segmentationId);
|
||||
const vtkUtils = {
|
||||
vtkImageMarchingSquares,
|
||||
vtkDataArray,
|
||||
vtkImageData,
|
||||
};
|
||||
|
||||
const RTSS = generateRTSSFromSegmentations(
|
||||
// inject colors to the segmentIndex
|
||||
const firstRepresentation =
|
||||
segmentationService.getRepresentationsForSegmentation(segmentationId)[0];
|
||||
Object.entries(segmentations.segments).forEach(([segmentIndex, segment]) => {
|
||||
segment.color = segmentationService.getSegmentColor(
|
||||
firstRepresentation.viewportId,
|
||||
segmentationId,
|
||||
segmentIndex
|
||||
);
|
||||
});
|
||||
|
||||
const RTSS = await generateRTSSFromSegmentations(
|
||||
segmentations,
|
||||
classes.MetadataProvider,
|
||||
DicomMetadataStore,
|
||||
cache,
|
||||
cornerstoneToolsEnums,
|
||||
vtkUtils
|
||||
DicomMetadataStore
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -303,48 +307,9 @@ const commandsModule = ({
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
setBrushSize: ({ value, toolNames }) => {
|
||||
const brushSize = Number(value);
|
||||
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
if (toolNames?.length === 0) {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||||
} else {
|
||||
toolNames?.forEach(toolName => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setThresholdRange: ({
|
||||
value,
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
}) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolNames?.forEach(toolName => {
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: {
|
||||
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
||||
},
|
||||
/**
|
||||
* Obsolete?
|
||||
*/
|
||||
loadSegmentationsForViewport: {
|
||||
commandFn: actions.loadSegmentationsForViewport,
|
||||
},
|
||||
@@ -361,12 +326,6 @@ const commandsModule = ({
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
setBrushSize: {
|
||||
commandFn: actions.setBrushSize,
|
||||
},
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { metaData, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7'];
|
||||
|
||||
const loadPromises = {};
|
||||
|
||||
@@ -33,7 +34,7 @@ function _getDisplaySetsFromSeries(
|
||||
const displaySet = {
|
||||
Modality: 'SEG',
|
||||
loading: false,
|
||||
isReconstructable: true, // by default for now since it is a volumetric SEG currently
|
||||
isReconstructable: false,
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
@@ -57,6 +58,7 @@ function _getDisplaySetsFromSeries(
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SEG')}`,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@@ -89,12 +91,14 @@ function _getDisplaySetsFromSeries(
|
||||
const addedDisplaySet = displaySetsAdded[0];
|
||||
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
|
||||
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.isReconstructable = addedDisplaySet.isReconstructable;
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
|
||||
}
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
@@ -177,12 +181,16 @@ async function _loadSegments({
|
||||
throw new Error('referencedDisplaySet is missing for SEG');
|
||||
}
|
||||
|
||||
const { instances: images } = referencedDisplaySet;
|
||||
const imageIds = images.map(({ imageId }) => imageId);
|
||||
let { imageIds } = referencedDisplaySet;
|
||||
|
||||
if (!imageIds) {
|
||||
// try images
|
||||
const { images } = referencedDisplaySet;
|
||||
imageIds = images.map(image => image.imageId);
|
||||
}
|
||||
|
||||
// Todo: what should be defaults here
|
||||
const tolerance = 0.001;
|
||||
const skipOverlapping = true;
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
@@ -190,11 +198,10 @@ async function _loadSegments({
|
||||
});
|
||||
});
|
||||
|
||||
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
|
||||
const results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
metaData,
|
||||
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
||||
{ metadataProvider: metaData, tolerance }
|
||||
);
|
||||
|
||||
let usedRecommendedDisplayCIELabValue = true;
|
||||
@@ -211,15 +218,6 @@ async function _loadSegments({
|
||||
}
|
||||
});
|
||||
|
||||
if (results.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message:
|
||||
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (!usedRecommendedDisplayCIELabValue) {
|
||||
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
|
||||
uiNotificationService.show({
|
||||
@@ -238,7 +236,8 @@ function _segmentationExists(segDisplaySet) {
|
||||
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.hasSegmentation',
|
||||
evaluate: ({ viewportId }) => {
|
||||
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
return {
|
||||
disabled: !segmentations?.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
@@ -12,27 +21,39 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright !bg-black opacity-50',
|
||||
disabledText: disabledText ?? 'No segmentations available',
|
||||
};
|
||||
}
|
||||
|
||||
const activeSegmentation = segmentationService.getActiveSegmentation(viewportId);
|
||||
if (!Object.keys(activeSegmentation.segments).length) {
|
||||
return {
|
||||
disabled: true,
|
||||
disabledText: 'Add segment to enable this tool',
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
if (!toolNames) {
|
||||
return {
|
||||
disabled: false,
|
||||
// isActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
@@ -43,12 +64,6 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
function createSEGToolGroupAndAddTools({
|
||||
commandsManager,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
toolGroupId,
|
||||
}) {
|
||||
const tools = customizationService.getCustomization('cornerstone.overlayViewportTools');
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
const updatedTools = commandsManager.run('initializeSegmentLabelTool', { tools });
|
||||
|
||||
return toolGroupService.createToolGroupAndAddTools(toolGroupId, updatedTools);
|
||||
}
|
||||
|
||||
export default createSEGToolGroupAndAddTools;
|
||||
@@ -1,10 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
HYDRATE_SEG: 5,
|
||||
};
|
||||
import { utils, Types } from '@ohif/extension-cornerstone';
|
||||
|
||||
function promptHydrateSEG({
|
||||
servicesManager,
|
||||
@@ -12,71 +6,20 @@ function promptHydrateSEG({
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
hydrateCallback,
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
const extensionManager = servicesManager._extensionManager;
|
||||
const appConfig = extensionManager._appConfig;
|
||||
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = appConfig?.disableConfirmationPrompts
|
||||
? RESPONSE.HYDRATE_SEG
|
||||
: await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
callback();
|
||||
});
|
||||
|
||||
window.setTimeout(async () => {
|
||||
const isHydrated = await hydrateCallback({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
}: {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
segDisplaySet: AppTypes.DisplaySet;
|
||||
viewportId: string;
|
||||
preHydrateCallbacks?: Types.HydrationCallback[];
|
||||
hydrateCallback: Types.HydrationCallback;
|
||||
}) {
|
||||
return utils.promptHydrationDialog({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
displaySet: segDisplaySet as AppTypes.DisplaySet,
|
||||
preHydrateCallbacks,
|
||||
hydrateCallback,
|
||||
type: 'SEG',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
|
||||
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
|
||||
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SEGViewport');
|
||||
const { servicesManager, commandsManager } = useSystem();
|
||||
const { children, displaySets, viewportOptions } = props as {
|
||||
children: React.ReactNode;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
viewportOptions: AppTypes.ViewportOptions;
|
||||
};
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
const { displaySetService, toolGroupService, segmentationService, customizationService } =
|
||||
servicesManager.services;
|
||||
|
||||
const LoadingIndicatorTotalPercent = customizationService.getCustomization(
|
||||
'ui.loadingIndicatorTotalPercent'
|
||||
);
|
||||
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
|
||||
@@ -40,16 +35,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
|
||||
// Hydration means that the SEG is opened and segments are loaded into the
|
||||
// segmentation panel, and SEG is also rendered on any viewport that is in the
|
||||
// same frameOfReferenceUID as the referencedSeriesUID of the SEG. However,
|
||||
// loading basically means SEG loading over network and bit unpacking of the
|
||||
// SEG data.
|
||||
const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated);
|
||||
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
||||
const [element, setElement] = useState(null);
|
||||
const [processingProgress, setProcessingProgress] = useState({
|
||||
percentComplete: null,
|
||||
totalSegments: null,
|
||||
@@ -61,6 +54,21 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
// If the referencedDisplaySetInstanceUID is not found, it means the SEG series is being
|
||||
// launched without its corresponding referenced display set (e.g., the SEG series is launched using
|
||||
// series launch /mode?StudyInstanceUIDs=&SeriesInstanceUID).
|
||||
// In such cases, we attempt to handle this scenario gracefully by
|
||||
// invoking a custom handler. Ideally, if a user tries to launch a series that isn't viewable,
|
||||
// (eg.: we can prompt them with an explanation and provide a link to the full study).
|
||||
if (!referencedDisplaySetInstanceUID) {
|
||||
const missingReferenceDisplaySetHandler = customizationService.getCustomization(
|
||||
'missingReferenceDisplaySetHandler'
|
||||
);
|
||||
const { handled } = missingReferenceDisplaySetHandler();
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
referencedDisplaySetInstanceUID
|
||||
);
|
||||
@@ -74,36 +82,10 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
displaySet: referencedDisplaySet,
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
/**
|
||||
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||
* has enabled the element. Note: we delegate all the image rendering to
|
||||
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||
* the image rendering, element enabling etc.
|
||||
*/
|
||||
const onElementEnabled = evt => {
|
||||
setElement(evt.detail.element);
|
||||
};
|
||||
|
||||
const onElementDisabled = () => {
|
||||
setElement(null);
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
<Component
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
displaySets={[segDisplaySet]}
|
||||
viewportOptions={{
|
||||
@@ -115,78 +97,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
}}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
/>
|
||||
);
|
||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { segments } = segmentation;
|
||||
|
||||
const numberOfSegments = Object.keys(segments).length;
|
||||
|
||||
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||
|
||||
// Segment 0 is always background
|
||||
|
||||
if (newSelectedSegmentIndex > numberOfSegments - 1) {
|
||||
newSelectedSegmentIndex = 1;
|
||||
} else if (newSelectedSegmentIndex === 0) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
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]);
|
||||
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segIsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if not active viewport, return
|
||||
if (viewportId !== activeViewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateCallback: hydrateSEG,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
hydrateCallback: async () => {
|
||||
await commandsManager.runCommand('hydrateSecondaryDisplaySet', {
|
||||
displaySet: segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, commandsManager, activeViewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
// on new seg display set, remove all segmentations from all viewports
|
||||
@@ -198,6 +137,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
|
||||
if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) {
|
||||
const { firstSegmentedSliceImageId } = segDisplaySet;
|
||||
const { presentationIds } = viewportOptions;
|
||||
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewReference: {
|
||||
referencedImageId: firstSegmentedSliceImageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -264,7 +214,12 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
|
||||
// This creates a custom tool group which has the lifetime of this view
|
||||
// only, and does NOT interfere with currently displayed segmentations.
|
||||
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
toolGroup = createSEGToolGroupAndAddTools({
|
||||
commandsManager,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
toolGroupId,
|
||||
});
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
@@ -276,54 +231,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
hydrateSEG();
|
||||
}, [storePresentationState, hydrateSEG]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<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 (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,11 +34,11 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.12",
|
||||
"@ohif/extension-measurement-tracking": "3.10.0-beta.12",
|
||||
"@ohif/ui": "3.10.0-beta.12",
|
||||
"dcmjs": "*",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/extension-cornerstone": "3.11.0",
|
||||
"@ohif/extension-measurement-tracking": "3.11.0",
|
||||
"@ohif/ui": "3.11.0",
|
||||
"dcmjs": "0.43.1",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.2.20",
|
||||
"@cornerstonejs/core": "^2.2.20",
|
||||
"@cornerstonejs/tools": "^2.2.20",
|
||||
"@cornerstonejs/adapters": "^3.32.5",
|
||||
"@cornerstonejs/core": "^3.32.5",
|
||||
"@cornerstonejs/tools": "^3.32.5",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,34 @@ import dcmjs from 'dcmjs';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
const { log } = OHIF;
|
||||
|
||||
interface Options {
|
||||
SeriesDescription?: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: number;
|
||||
InstanceNumber?: number;
|
||||
SeriesDate?: string;
|
||||
SeriesTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
*
|
||||
*/
|
||||
const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
|
||||
const _generateReport = (measurementData, additionalFindingTypes, options: Options = {}) => {
|
||||
const filteredToolState = getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
);
|
||||
|
||||
const report = MeasurementReport.generateReport(
|
||||
filteredToolState,
|
||||
metaData,
|
||||
utilities.worldToImageCoords,
|
||||
options
|
||||
);
|
||||
const report = MeasurementReport.generateReport(filteredToolState, metaData, options);
|
||||
|
||||
const { dataset } = report;
|
||||
|
||||
@@ -37,13 +41,37 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||
}
|
||||
|
||||
dataset.InstanceNumber = options.InstanceNumber ?? 1;
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
const commandsModule = (props: withAppTypes) => {
|
||||
const { servicesManager } = props;
|
||||
const { servicesManager, extensionManager, commandsManager } = props;
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
changeColorMeasurement: ({ uid }) => {
|
||||
// When this gets supported, it probably belongs in cornerstone, not sr
|
||||
throw new Error('Unsupported operation: changeColorMeasurement');
|
||||
// const { color } = measurementService.getMeasurement(uid);
|
||||
// const rgbaColor = {
|
||||
// r: color[0],
|
||||
// g: color[1],
|
||||
// b: color[2],
|
||||
// a: color[3] / 255.0,
|
||||
// };
|
||||
// colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||
// if (actionId === 'cancel') {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
|
||||
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
|
||||
// });
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param measurementData An array of measurements from the measurements service
|
||||
@@ -53,7 +81,7 @@ const commandsModule = (props: withAppTypes) => {
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
|
||||
const srDataset = actions.generateReport(measurementData, additionalFindingTypes, options);
|
||||
const srDataset = _generateReport(measurementData, additionalFindingTypes, options);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
@@ -96,13 +124,15 @@ const commandsModule = (props: withAppTypes) => {
|
||||
console.log('naturalizedReport missing imaging content', naturalizedReport);
|
||||
throw new Error('Invalid report, no content');
|
||||
}
|
||||
if (!naturalizedReport.SOPClassUID) {
|
||||
throw new Error('No sop class uid');
|
||||
}
|
||||
|
||||
const onBeforeDicomStore =
|
||||
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
|
||||
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
|
||||
|
||||
let dicomDict;
|
||||
if (typeof onBeforeDicomStore === 'function') {
|
||||
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
|
||||
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
|
||||
@@ -123,15 +153,23 @@ const commandsModule = (props: withAppTypes) => {
|
||||
throw new Error(error.message || 'Error while saving the measurements.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads measurements by hydrating and loading the SR for the given display set instance UID
|
||||
* and displays it in the active viewport.
|
||||
*/
|
||||
hydrateStructuredReport: ({ displaySetInstanceUID }) => {
|
||||
return hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, commandsManager },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
downloadReport: {
|
||||
commandFn: actions.downloadReport,
|
||||
},
|
||||
storeMeasurements: {
|
||||
commandFn: actions.storeMeasurements,
|
||||
},
|
||||
downloadReport: actions.downloadReport,
|
||||
storeMeasurements: actions.storeMeasurements,
|
||||
hydrateStructuredReport: actions.hydrateStructuredReport,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+29
-284
@@ -1,38 +1,24 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
import { usePositionPresentationStore, OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core/src/contextProviders/SystemProvider';
|
||||
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
const {
|
||||
commandsManager,
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
function OHIFCornerstoneSRMeasurementViewport(props) {
|
||||
const { servicesManager } = useSystem();
|
||||
const { children, dataSource, displaySets, viewportOptions } = props as {
|
||||
children: React.ReactNode;
|
||||
dataSource: unknown;
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
viewportOptions: AppTypes.ViewportOptions;
|
||||
};
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
measurementService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
@@ -43,66 +29,15 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
|
||||
const srDisplaySet = displaySets[0];
|
||||
|
||||
const { setPositionPresentation } = usePositionPresentationStore();
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||
const [measurementCount, setMeasurementCount] = useState(1);
|
||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
|
||||
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
|
||||
const [element, setElement] = useState(null);
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
|
||||
// Optional hook into tracking extension, if present.
|
||||
let trackedMeasurements;
|
||||
let sendTrackedMeasurementsEvent;
|
||||
|
||||
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
|
||||
MEASUREMENT_TRACKING_EXTENSION_ID
|
||||
);
|
||||
|
||||
if (hasMeasurementTrackingExtension) {
|
||||
const contextModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
|
||||
);
|
||||
|
||||
const tracked = useContext(contextModule.context);
|
||||
trackedMeasurements = tracked?.[0];
|
||||
sendTrackedMeasurementsEvent = tracked?.[1];
|
||||
}
|
||||
|
||||
if (!sendTrackedMeasurementsEvent) {
|
||||
// if no panels from measurement-tracking extension is used, this code will run
|
||||
trackedMeasurements = null;
|
||||
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
|
||||
measurementService.clearMeasurements();
|
||||
const { SeriesInstanceUIDs } = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||
if (displaySets.length) {
|
||||
viewportGridService.setDisplaySetsForViewports([
|
||||
{
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
/**
|
||||
* Store the tracking identifiers per viewport in order to be able to
|
||||
* show the SR measurements on the referenced image on the correct viewport,
|
||||
* when multiple viewports are used.
|
||||
*/
|
||||
const setTrackingIdentifiers = useCallback(
|
||||
measurementSelected => {
|
||||
const { measurements } = srDisplaySet;
|
||||
@@ -128,22 +63,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
|
||||
const updateViewport = useCallback(
|
||||
newMeasurementSelected => {
|
||||
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
|
||||
const { StudyInstanceUID, displaySetInstanceUID } = srDisplaySet;
|
||||
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
// Todo: what happens if there are multiple SOP Classes? Why we are
|
||||
// not throwing an error?
|
||||
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||
}
|
||||
|
||||
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
_getViewportReferencedDisplaySetData(
|
||||
srDisplaySet,
|
||||
newMeasurementSelected,
|
||||
@@ -154,33 +79,17 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
}
|
||||
|
||||
setMeasurementSelected(newMeasurementSelected);
|
||||
|
||||
setActiveImageDisplaySetData(referencedDisplaySet);
|
||||
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
|
||||
|
||||
if (
|
||||
referencedDisplaySet.displaySetInstanceUID ===
|
||||
activeImageDisplaySetData?.displaySetInstanceUID
|
||||
) {
|
||||
const { measurements } = srDisplaySet;
|
||||
|
||||
// it means that we have a new referenced display set, and the
|
||||
// imageIdIndex will handle it by updating the viewport, but if they
|
||||
// are the same we just need to use measurementService to jump to the
|
||||
// new measurement
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!csViewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = csViewport.getImageIds();
|
||||
|
||||
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
|
||||
|
||||
if (imageIdIndex !== -1) {
|
||||
csViewport.setImageIdIndex(imageIdIndex);
|
||||
}
|
||||
}
|
||||
const { presentationIds } = viewportOptions;
|
||||
const measurement = srDisplaySet.measurements[newMeasurementSelected];
|
||||
setPositionPresentation(presentationIds.positionPresentationId, {
|
||||
viewReference: {
|
||||
referencedImageId: measurement.imageId,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
|
||||
@@ -191,10 +100,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const { measurements } = srDisplaySet;
|
||||
const measurement = measurements[measurementSelected];
|
||||
|
||||
@@ -202,12 +107,8 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
|
||||
image => image.imageId === measurement.imageId
|
||||
);
|
||||
|
||||
return (
|
||||
<Component
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
// should be passed second since we don't want SR displaySet to
|
||||
// override the activeImageDisplaySetData
|
||||
@@ -230,29 +131,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
/>
|
||||
);
|
||||
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
|
||||
|
||||
const onMeasurementChange = useCallback(
|
||||
direction => {
|
||||
let newMeasurementSelected = measurementSelected;
|
||||
|
||||
newMeasurementSelected += direction;
|
||||
if (newMeasurementSelected >= measurementCount) {
|
||||
newMeasurementSelected = 0;
|
||||
} else if (newMeasurementSelected < 0) {
|
||||
newMeasurementSelected = measurementCount - 1;
|
||||
}
|
||||
|
||||
setTrackingIdentifiers(newMeasurementSelected);
|
||||
updateViewport(newMeasurementSelected);
|
||||
},
|
||||
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
|
||||
);
|
||||
|
||||
/**
|
||||
Cleanup the SR viewport when the viewport is destroyed
|
||||
*/
|
||||
@@ -287,8 +170,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
await srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
updateViewport(measurementSelected);
|
||||
};
|
||||
loadSR();
|
||||
@@ -311,50 +192,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
|
||||
updateSR();
|
||||
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
}, [trackedMeasurements]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportId,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
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;
|
||||
|
||||
@@ -391,8 +228,6 @@ OHIFCornerstoneSRMeasurementViewport.propTypes = {
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
viewportOptions: PropTypes.object,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
async function _getViewportReferencedDisplaySetData(
|
||||
@@ -417,6 +252,9 @@ async function _getViewportReferencedDisplaySetData(
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!referencedDisplaySet?.images) {
|
||||
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
|
||||
}
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
@@ -436,97 +274,4 @@ async function _getViewportReferencedDisplaySetData(
|
||||
return { referencedDisplaySetMetadata, referencedDisplaySet };
|
||||
}
|
||||
|
||||
function _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportId,
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}) {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
// 2 - Locked
|
||||
// 3 - Rehydratable / Open
|
||||
const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
This structured report is not compatible
|
||||
<br />
|
||||
with this application.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
StatusIcon = () => <Icon name="status-locked" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
This structured report is currently read-only
|
||||
<br />
|
||||
because you are tracking measurements in
|
||||
<br />
|
||||
another viewport.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</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">SR</span>
|
||||
</div>
|
||||
{state === 3 && (
|
||||
<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={handleMouseUp}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusArea />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneSRMeasurementViewport;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
@@ -14,8 +15,12 @@ import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
|
||||
|
||||
const { sopClassDictionary } = utils;
|
||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
const { MetadataProvider: metadataProvider } = classes;
|
||||
const {
|
||||
TEXT_ANNOTATION_POSITION,
|
||||
COMMENT_CODE,
|
||||
CodeScheme: Cornerstone3DCodeScheme,
|
||||
} = adaptersSR.Cornerstone3D;
|
||||
|
||||
type InstanceMetadata = Types.InstanceMetadata;
|
||||
|
||||
@@ -90,6 +95,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
ConceptNameCodeSequence,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
@@ -106,6 +112,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
@@ -120,6 +127,7 @@ function _getDisplaySetsFromSeries(
|
||||
sopClassUids,
|
||||
instance,
|
||||
addInstances,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
|
||||
};
|
||||
|
||||
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
||||
@@ -212,6 +220,12 @@ async function _load(
|
||||
});
|
||||
}
|
||||
|
||||
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
|
||||
return (
|
||||
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if measurements can be added to a display set.
|
||||
*
|
||||
@@ -232,18 +246,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
measurement => measurement.loaded === false
|
||||
);
|
||||
|
||||
if (
|
||||
unloadedMeasurements.length === 0 ||
|
||||
!(newDisplaySet instanceof ImageSet) ||
|
||||
newDisplaySet.unsupported
|
||||
) {
|
||||
if (!unloadedMeasurements.length || newDisplaySet.unsupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const { sopClassUids } = newDisplaySet;
|
||||
// Create a Set for faster lookups
|
||||
// const sopClassUidSet = new Set(sopClassUids);
|
||||
|
||||
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
|
||||
const imageIdMap = new Map<string, string>();
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
||||
@@ -262,10 +268,11 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
let measurement = unloadedMeasurements[j];
|
||||
const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D';
|
||||
|
||||
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
|
||||
const onBeforeSRAddMeasurement = customizationService.getCustomization(
|
||||
'onBeforeSRAddMeasurement'
|
||||
)?.value;
|
||||
);
|
||||
|
||||
if (typeof onBeforeSRAddMeasurement === 'function') {
|
||||
measurement = onBeforeSRAddMeasurement({
|
||||
@@ -276,9 +283,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
}
|
||||
|
||||
// if it is 3d SR we can just add the SR annotation
|
||||
if (is3DSR) {
|
||||
if (
|
||||
is3DSR &&
|
||||
is3DMeasurement &&
|
||||
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
|
||||
) {
|
||||
addSRAnnotation(measurement, null, null);
|
||||
measurement.loaded = true;
|
||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -352,7 +365,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
||||
* @param {Object} options.extensionManager - The extension manager.
|
||||
* @returns {Array} An array containing the SOP class handler module.
|
||||
*/
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||
const { servicesManager, extensionManager } = params;
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
@@ -464,11 +478,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
|
||||
* @returns {any} - The processed measurement result.
|
||||
*/
|
||||
function _processMeasurement(mergedContentSequence) {
|
||||
if (
|
||||
mergedContentSequence.some(
|
||||
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||
)
|
||||
) {
|
||||
if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
|
||||
return _processTID1410Measurement(mergedContentSequence);
|
||||
}
|
||||
|
||||
@@ -506,12 +516,25 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
|
||||
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
||||
|
||||
const { ConceptNameCodeSequence: conceptNameItem } = graphicItem;
|
||||
const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem;
|
||||
const graphicCode = `${graphicDesignator}:${graphicValue}`;
|
||||
|
||||
const pointDataItem = _getCoordsFromSCOORDOrSCOORD3D(graphicItem);
|
||||
const is3DMeasurement = pointDataItem.ValueType === 'SCOORD3D';
|
||||
const pointLength = is3DMeasurement ? 3 : 2;
|
||||
const pointsLength = pointDataItem.GraphicData.length / pointLength;
|
||||
|
||||
const measurement = {
|
||||
loaded: false,
|
||||
labels: [],
|
||||
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
||||
coords: [pointDataItem],
|
||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
graphicCode,
|
||||
is3DMeasurement,
|
||||
pointsLength,
|
||||
graphicType: pointDataItem.GraphicType,
|
||||
};
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
@@ -562,6 +585,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
|
||||
);
|
||||
|
||||
const commentSites = mergedContentSequence.filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator &&
|
||||
item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
loaded: false,
|
||||
labels: [],
|
||||
@@ -570,6 +599,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
};
|
||||
|
||||
if (commentSites) {
|
||||
for (const group of commentSites) {
|
||||
if (group.TextValue) {
|
||||
measurement.labels.push({ label: group.TextValue, value: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
finding &&
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
@@ -721,4 +758,17 @@ function _getSequenceAsArray(sequence) {
|
||||
return Array.isArray(sequence) ? sequence : [sequence];
|
||||
}
|
||||
|
||||
function isScoordOr3d(group) {
|
||||
return group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D';
|
||||
}
|
||||
|
||||
function isTextPosition(group) {
|
||||
const concept = group.ConceptNameCodeSequence[0];
|
||||
return (
|
||||
concept &&
|
||||
concept.CodeValue === TEXT_ANNOTATION_POSITION.value &&
|
||||
concept.CodingSchemeDesignator === TEXT_ANNOTATION_POSITION.schemeDesignator
|
||||
);
|
||||
}
|
||||
|
||||
export default getSopClassHandlerModule;
|
||||
@@ -30,6 +30,7 @@ const dicomSRExtension = {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeEnter,
|
||||
|
||||
preRegistration,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
utilities as csToolsUtils,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { Types, MeasurementService } from '@ohif/core';
|
||||
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
||||
@@ -57,7 +56,7 @@ export default function init({
|
||||
'SRSCOORD3DPoint',
|
||||
POINT,
|
||||
SRSCOOR3DProbeMapper.toAnnotation,
|
||||
SRSCOOR3DProbeMapper.toMeasurement
|
||||
SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
|
||||
);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
@@ -77,28 +76,4 @@ export default function init({
|
||||
SRRectangleROI: dashedLine,
|
||||
global: {},
|
||||
});
|
||||
|
||||
measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
|
||||
({ viewportId, measurement, isConsumed }) => {
|
||||
if (isConsumed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const { viewPlaneNormal } = currentViewport.getCamera();
|
||||
const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport(
|
||||
currentViewport as StackViewport,
|
||||
measurement.points[0],
|
||||
viewPlaneNormal
|
||||
);
|
||||
const imageIndex = (currentViewport as StackViewport)
|
||||
.getImageIds()
|
||||
.indexOf(referencedImageId);
|
||||
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
|
||||
} catch (error) {
|
||||
console.warn('Unable to jump to image based on measurement coordinate', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { type Types } from '@cornerstonejs/core';
|
||||
import {
|
||||
annotation,
|
||||
drawing,
|
||||
|
||||
@@ -7,13 +7,8 @@ const SRSCOOR3DProbe = {
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
displaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType,
|
||||
customizationService
|
||||
) => {
|
||||
toMeasurement: ({ servicesManager, getValueTypeFromToolType }, csToolsEventDetail) => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const { annotation } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
@@ -22,14 +17,25 @@ const SRSCOOR3DProbe = {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName } = metadata;
|
||||
const { toolName, FrameOfReferenceUID } = metadata;
|
||||
const { points } = data.handles;
|
||||
|
||||
const displaySets = displaySetService
|
||||
.getActiveDisplaySets()
|
||||
.filter(ds => ds.FrameOfReferenceUID === FrameOfReferenceUID);
|
||||
const displaySet = displaySets.filter(ds => ds.isReconstructable)[0] || displaySets[0];
|
||||
|
||||
const { StudyInstanceUID: referenceStudyUID, SeriesInstanceUID: referenceSeriesUID } =
|
||||
displaySets[0] || {};
|
||||
|
||||
const displayText = getDisplayText(annotation);
|
||||
return {
|
||||
uid: annotationUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceStudyUID,
|
||||
referenceSeriesUID,
|
||||
displaySetInstanceUID: displaySet?.displaySetInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
label: data.label,
|
||||
displayText: displayText,
|
||||
@@ -56,7 +62,10 @@ function getDisplayText(annotation) {
|
||||
}
|
||||
}
|
||||
|
||||
return displayText;
|
||||
return {
|
||||
primary: displayText,
|
||||
secondary: [],
|
||||
};
|
||||
}
|
||||
|
||||
export default SRSCOOR3DProbe;
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Types, annotation } from '@cornerstonejs/tools';
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
import getRenderableData from './getRenderableData';
|
||||
import toolNames from '../tools/toolNames';
|
||||
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
|
||||
export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
||||
let toolName = toolNames.DICOMSRDisplay;
|
||||
const renderableData = measurement.coords.reduce((acc, coordProps) => {
|
||||
@@ -25,7 +28,12 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
||||
}
|
||||
|
||||
if (valueType === 'SCOORD3D') {
|
||||
toolName = toolNames.SRSCOORD3DPoint;
|
||||
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(
|
||||
measurement.TrackingIdentifier
|
||||
);
|
||||
if (!adapter) {
|
||||
toolName = toolNames.SRSCOORD3DPoint;
|
||||
}
|
||||
|
||||
// get the ReferencedFrameOfReferenceUID from the measurement
|
||||
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
|
||||
@@ -63,5 +71,4 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
||||
* was not triggering annotation_added events.
|
||||
*/
|
||||
annotation.state.addAnnotation(SRAnnotation);
|
||||
console.debug('Adding SR annotation:', SRAnnotation);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const contentItemFormatters = {
|
||||
return `${NumericValue} ${CodeValue}`;
|
||||
},
|
||||
PNAME: contentItem => {
|
||||
const personName = contentItem.PersonName?.[0]?.Alphabetic;
|
||||
const personName = contentItem.PersonName?.[0];
|
||||
return personName ? utils.formatPN(personName) : undefined;
|
||||
},
|
||||
DATE: contentItem => {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
const { log } = OHIF;
|
||||
import { NO_IMAGE_ID } from '@cornerstonejs/adapters';
|
||||
|
||||
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
|
||||
const filteredToolState = {};
|
||||
|
||||
function addToFilteredToolState(annotation, toolType) {
|
||||
if (!annotation.metadata?.referencedImageId) {
|
||||
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = annotation.metadata.referencedImageId;
|
||||
const imageId = annotation.metadata?.referencedImageId ?? NO_IMAGE_ID;
|
||||
|
||||
if (!filteredToolState[imageId]) {
|
||||
filteredToolState[imageId] = {};
|
||||
|
||||
@@ -11,7 +11,11 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
* @returns {string} The extracted label.
|
||||
*/
|
||||
export default function getLabelFromDCMJSImportedToolData(toolData) {
|
||||
const { findingSites = [], finding } = toolData;
|
||||
const { findingSites = [], finding, annotation } = toolData;
|
||||
|
||||
if (annotation.data.label) {
|
||||
return annotation.data.label;
|
||||
}
|
||||
|
||||
let freeTextLabel = findingSites.find(
|
||||
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
||||
|
||||
@@ -36,6 +36,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
|
||||
ValueType,
|
||||
imageId,
|
||||
});
|
||||
|
||||
if (!imageId) {
|
||||
// without the image id it's not possible to perform the calculations below
|
||||
// these calculations also do not seem to be needed, since everything works
|
||||
// just fine when we skip them. At least for SCOORD3D annotations.
|
||||
return pointsWorld;
|
||||
}
|
||||
|
||||
// We do not have an explicit draw circle svg helper in Cornerstone3D at
|
||||
// this time, but we can use the ellipse svg helper to draw a circle, so
|
||||
// here we reshape the data for that purpose.
|
||||
@@ -90,6 +98,13 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
|
||||
imageId,
|
||||
});
|
||||
|
||||
if (!imageId) {
|
||||
// without the image id it's not possible to perform the calculations below
|
||||
// these calculations also do not seem to be needed, since everything works
|
||||
// just fine when we skip them. At least for SCOORD3D annotations.
|
||||
return pointsWorld;
|
||||
}
|
||||
|
||||
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
|
||||
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
|
||||
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { utilities, metaData } from '@cornerstonejs/core';
|
||||
import { utilities, metaData, type Types } from '@cornerstonejs/core';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
|
||||
import { annotation as CsAnnotation, type Types as ToolTypes } from '@cornerstonejs/tools';
|
||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||
|
||||
const { locking } = CsAnnotation;
|
||||
const { guid } = OHIF.utils;
|
||||
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
const convertCode = (codingValues, code) => {
|
||||
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
|
||||
@@ -37,26 +38,20 @@ const convertSites = (codingValues, sites) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Hydrates a structured report, for default viewports.
|
||||
*
|
||||
* Hydrates a structured report
|
||||
* Handles 2d and 3d hydration from SCOORD and SCOORD3D points
|
||||
* For 3D hydration, chooses a volume display set to display with
|
||||
* FOr 2D hydration, chooses the (first) display set containing the referenced image.
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
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 codingValues = customizationService.getCustomization('codingValues');
|
||||
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
@@ -93,9 +88,10 @@ export default function hydrateStructuredReport(
|
||||
}
|
||||
});
|
||||
|
||||
const datasetToUse = _mapLegacyDataSet(instance);
|
||||
// Mapping of legacy datasets is now directly handled by adapters module
|
||||
const datasetToUse = instance;
|
||||
|
||||
// Use dcmjs to generate toolState.
|
||||
// Use CS3D adapters to generate toolState.
|
||||
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||
datasetToUse,
|
||||
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
|
||||
@@ -103,12 +99,10 @@ export default function hydrateStructuredReport(
|
||||
// that measurements were added to the display set are the same order as
|
||||
// the measurementGroups in the instance.
|
||||
sopInstanceUIDToImageId,
|
||||
utilities.imageToWorldCoords,
|
||||
metaData
|
||||
);
|
||||
|
||||
const onBeforeSRHydration =
|
||||
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
|
||||
const onBeforeSRHydration = customizationService.getCustomization('onBeforeSRHydration')?.value;
|
||||
|
||||
if (typeof onBeforeSRHydration === 'function') {
|
||||
storedMeasurementByAnnotationType = onBeforeSRHydration({
|
||||
@@ -155,6 +149,9 @@ export default function hydrateStructuredReport(
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
if (!imageId) {
|
||||
continue;
|
||||
}
|
||||
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
|
||||
|
||||
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
||||
@@ -168,38 +165,55 @@ export default function hydrateStructuredReport(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets reference data for what frame of reference and the referenced
|
||||
* image id, or for 3d measurements, the volumeId to apply this annotation to.
|
||||
*/
|
||||
function getReferenceData(toolData): ToolTypes.AnnotationMetadata {
|
||||
// Add the measurement to toolState
|
||||
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
||||
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
||||
// in the wrong value.
|
||||
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const imageId =
|
||||
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
||||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
||||
|
||||
if (!imageId) {
|
||||
return getReferenceData3D(toolData, servicesManager);
|
||||
}
|
||||
|
||||
const instance = metaData.get('instance', imageId);
|
||||
const {
|
||||
FrameOfReferenceUID,
|
||||
// SOPInstanceUID,
|
||||
// SeriesInstanceUID,
|
||||
// StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
return {
|
||||
referencedImageId: imageId,
|
||||
FrameOfReferenceUID,
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
||||
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
||||
|
||||
toolDataForAnnotationType.forEach(toolData => {
|
||||
// Add the measurement to toolState
|
||||
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
||||
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
||||
// in the wrong value.
|
||||
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const imageId =
|
||||
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
||||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
||||
|
||||
toolData.uid = guid();
|
||||
|
||||
const instance = metaData.get('instance', imageId);
|
||||
const {
|
||||
FrameOfReferenceUID,
|
||||
// SOPInstanceUID,
|
||||
// SeriesInstanceUID,
|
||||
// StudyInstanceUID,
|
||||
} = instance;
|
||||
const referenceData = getReferenceData(toolData);
|
||||
const { imageId } = referenceData;
|
||||
|
||||
const annotation = {
|
||||
annotationUID: toolData.annotation.annotationUID,
|
||||
data: toolData.annotation.data,
|
||||
metadata: {
|
||||
...referenceData,
|
||||
toolName: annotationType,
|
||||
referencedImageId: imageId,
|
||||
FrameOfReferenceUID,
|
||||
},
|
||||
};
|
||||
utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata);
|
||||
|
||||
const source = measurementService.getSource(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
@@ -208,7 +222,11 @@ export default function hydrateStructuredReport(
|
||||
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
|
||||
annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
|
||||
annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
|
||||
annotation.data.site = annotation.data.findingSites?.[0];
|
||||
annotation.data.findingSites?.forEach(site => {
|
||||
if (site.type) {
|
||||
annotation.data[site.type] = site;
|
||||
}
|
||||
});
|
||||
|
||||
const matchingMapping = mappings.find(m => m.annotationType === annotationType);
|
||||
|
||||
@@ -220,12 +238,16 @@ export default function hydrateStructuredReport(
|
||||
dataSource
|
||||
);
|
||||
|
||||
commandsManager.runCommand('updateMeasurement', {
|
||||
uid: newAnnotationUID,
|
||||
code: annotation.data.finding,
|
||||
});
|
||||
|
||||
if (disableEditing) {
|
||||
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
|
||||
locking.setAnnotationLocked(addedAnnotation, true);
|
||||
locking.setAnnotationLocked(newAnnotationUID, true);
|
||||
}
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
if (imageId && !imageIds.includes(imageId)) {
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
@@ -239,61 +261,91 @@ export default function hydrateStructuredReport(
|
||||
};
|
||||
}
|
||||
|
||||
function _mapLegacyDataSet(dataset) {
|
||||
const REPORT = 'Imaging Measurements';
|
||||
const GROUP = 'Measurement Group';
|
||||
const TRACKING_IDENTIFIER = 'Tracking Identifier';
|
||||
|
||||
// Identify the Imaging Measurements
|
||||
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(
|
||||
codeMeaningEquals(REPORT)
|
||||
);
|
||||
|
||||
// Retrieve the Measurements themselves
|
||||
const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter(
|
||||
codeMeaningEquals(GROUP)
|
||||
);
|
||||
|
||||
// For each of the supported measurement types, compute the measurement data
|
||||
const measurementData = {};
|
||||
|
||||
const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
|
||||
const registeredToolClasses = [];
|
||||
|
||||
Object.keys(cornerstoneToolClasses).forEach(key => {
|
||||
registeredToolClasses.push(cornerstoneToolClasses[key]);
|
||||
measurementData[key] = [];
|
||||
});
|
||||
|
||||
measurementGroups.forEach((measurementGroup, index) => {
|
||||
const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence);
|
||||
|
||||
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
|
||||
contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
|
||||
);
|
||||
|
||||
const TrackingIdentifier = TrackingIdentifierGroup.TextValue;
|
||||
|
||||
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
|
||||
cornerstoneTag = CORNERSTONE_3D_TAG;
|
||||
}
|
||||
|
||||
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
|
||||
|
||||
TrackingIdentifierGroup.TextValue = mappedTrackingIdentifier;
|
||||
});
|
||||
|
||||
return dataset;
|
||||
/**
|
||||
* For 3d annotations, there are often several display sets which could
|
||||
* be used to display the annotation. Choose the first annotation with the
|
||||
* same frame of reference that is reconstructable, or the first display set
|
||||
* otherwise.
|
||||
*/
|
||||
function chooseDisplaySet(displaySets, annotation) {
|
||||
if (!displaySets?.length) {
|
||||
console.warn('No display set found for', annotation);
|
||||
return;
|
||||
}
|
||||
if (displaySets.length === 1) {
|
||||
return displaySets[0];
|
||||
}
|
||||
const volumeDs = displaySets.find(ds => ds.isReconstructable);
|
||||
if (volumeDs) {
|
||||
return volumeDs;
|
||||
}
|
||||
return displaySets[0];
|
||||
}
|
||||
|
||||
const toArray = function (x) {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
};
|
||||
/**
|
||||
* Gets the additional reference data appropriate for a 3d reference.
|
||||
* This will choose a volume id, frame of reference and a plane restriction.
|
||||
*/
|
||||
function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
|
||||
const { FrameOfReferenceUID } = toolData.annotation.metadata;
|
||||
const { points } = toolData.annotation.data.handles;
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const displaySetsFOR = displaySetService.getDisplaySetsBy(
|
||||
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID
|
||||
);
|
||||
if (!displaySetsFOR.length || !points?.length) {
|
||||
return {
|
||||
FrameOfReferenceUID,
|
||||
};
|
||||
}
|
||||
const ds = chooseDisplaySet(displaySetsFOR, toolData.annotation);
|
||||
const cameraView = chooseCameraView(ds, points);
|
||||
|
||||
const codeMeaningEquals = codeMeaningName => {
|
||||
return contentItem => {
|
||||
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
|
||||
const viewReference = {
|
||||
...cameraView,
|
||||
volumeId: ds.displaySetInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
};
|
||||
};
|
||||
utilities.updatePlaneRestriction(points, viewReference);
|
||||
return viewReference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses a possible camera view - right now this is fairly basic,
|
||||
* just setting the unknowns to null.
|
||||
*/
|
||||
function chooseCameraView(_ds, points) {
|
||||
const selectedPoints = choosePoints(points);
|
||||
const cameraFocalPoint = <Point3>centerOf(selectedPoints);
|
||||
// These are sufficient to be null for now and can be set on first view
|
||||
let viewPlaneNormal: Types.Point3 = null;
|
||||
let viewUp: Types.Point3 = null;
|
||||
|
||||
return {
|
||||
cameraFocalPoint,
|
||||
viewPlaneNormal,
|
||||
viewUp,
|
||||
};
|
||||
}
|
||||
|
||||
function centerOf(points) {
|
||||
const scale = 1 / points.length;
|
||||
const center = vec3.create();
|
||||
for (const point of points) {
|
||||
vec3.scaleAndAdd(center, center, point, scale);
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
function choosePoints(points) {
|
||||
if (points.length === 1 || points.length === 2) {
|
||||
return points;
|
||||
}
|
||||
const firstIndex = 0;
|
||||
const secondIndex = Math.ceil(points.length / 4);
|
||||
const thirdIndex = Math.ceil(points.length / 2);
|
||||
// TODO - check if colinear, if so try to find another 3 points.
|
||||
|
||||
const newPoints = [points[firstIndex], points[secondIndex], points[thirdIndex]];
|
||||
return newPoints;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const cornerstoneAdapters =
|
||||
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG;
|
||||
|
||||
/**
|
||||
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
|
||||
*
|
||||
* @param {object} displaySet The SR `displaySet` to check.
|
||||
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `measurementService`.
|
||||
* @returns {boolean} True if the SR can be rehydrated into the `measurementService`.
|
||||
*/
|
||||
export default function isRehydratable(displaySet, mappings) {
|
||||
if (!mappings || !mappings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||
const { measurements } = displaySet;
|
||||
|
||||
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||
adapterKey =>
|
||||
typeof cornerstoneAdapters[adapterKey].isValidCornerstoneTrackingIdentifier === 'function'
|
||||
);
|
||||
|
||||
const adapters = [];
|
||||
|
||||
adapterKeys.forEach(key => {
|
||||
if (mappingDefinitions.includes(key)) {
|
||||
// Must have both a dcmjs adapter and a measurementService
|
||||
// Definition in order to be a candidate for import.
|
||||
adapters.push(cornerstoneAdapters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < measurements.length; i++) {
|
||||
const { TrackingIdentifier } = measurements[i] || {};
|
||||
const hydratable = adapters.some(adapter => {
|
||||
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
|
||||
cornerstoneTag = CORNERSTONE_3D_TAG;
|
||||
}
|
||||
|
||||
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
|
||||
|
||||
return adapter.isValidCornerstoneTrackingIdentifier(mappedTrackingIdentifier);
|
||||
});
|
||||
|
||||
if (hydratable) {
|
||||
return true;
|
||||
}
|
||||
console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]);
|
||||
}
|
||||
|
||||
console.log('No measurements found which were rehydratable');
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
|
||||
/**
|
||||
* Checks if the given `displaySet`can be rehydrated into the `measurementService`.
|
||||
*
|
||||
* @param {object} displaySet The SR `displaySet` to check.
|
||||
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `measurementService`.
|
||||
* @returns {boolean} True if the SR can be rehydrated into the `measurementService`.
|
||||
*/
|
||||
export default function isRehydratable(displaySet, mappings) {
|
||||
if (!mappings || !mappings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mappingDefinitions = new Set<string>();
|
||||
for (const m of mappings) {
|
||||
mappingDefinitions.add(m.annotationType);
|
||||
}
|
||||
|
||||
const { measurements } = displaySet;
|
||||
|
||||
for (let i = 0; i < measurements.length; i++) {
|
||||
const measurement = measurements[i];
|
||||
if (!measurement) {
|
||||
continue;
|
||||
}
|
||||
const { TrackingIdentifier = '', graphicType, graphicCode, pointsLength } = measurement;
|
||||
if (!TrackingIdentifier && !graphicType) {
|
||||
console.warn('No tracking identifier or graphicType for measurement ', measurement);
|
||||
continue;
|
||||
}
|
||||
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(TrackingIdentifier);
|
||||
const adapters = MeasurementReport.getAdaptersForTypes(graphicCode, graphicType, pointsLength);
|
||||
const hydratable =
|
||||
(adapter && mappingDefinitions.has(adapter.toolType)) ||
|
||||
(adapters && adapters.some(adapter => mappingDefinitions.has(adapter.toolType)));
|
||||
|
||||
if (hydratable) {
|
||||
return true;
|
||||
}
|
||||
console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]);
|
||||
}
|
||||
|
||||
console.log('No measurements found which were rehydratable');
|
||||
return false;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "OHIF extension for 4D volumes data",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,12 +29,12 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/extension-cornerstone": "3.10.0-beta.12",
|
||||
"@ohif/extension-default": "3.10.0-beta.12",
|
||||
"@ohif/i18n": "3.10.0-beta.12",
|
||||
"@ohif/ui": "3.10.0-beta.12",
|
||||
"dcmjs": "*",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/extension-cornerstone": "3.11.0",
|
||||
"@ohif/extension-default": "3.11.0",
|
||||
"@ohif/i18n": "3.11.0",
|
||||
"@ohif/ui": "3.11.0",
|
||||
"dcmjs": "0.43.1",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^2.2.20",
|
||||
"@cornerstonejs/tools": "^2.2.20",
|
||||
"@cornerstonejs/core": "^3.32.5",
|
||||
"@cornerstonejs/tools": "^3.32.5",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
-1
@@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
debugger;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
|
||||
@@ -263,7 +263,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate });
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: () => {
|
||||
// Todo: this assumes there is only one dynamic display set in the viewer
|
||||
@@ -313,7 +313,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate });
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { Toolbox } from '@ohif/extension-default';
|
||||
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
@@ -19,10 +21,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager, co
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
buttonSectionId={toolbarService.sections.dynamicToolbox}
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
<PanelSegmentation
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
IconButton,
|
||||
InputNumber,
|
||||
Icon,
|
||||
Icons,
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { DoubleSlider } from '@ohif/ui-next';
|
||||
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Numeric,
|
||||
} from '@ohif/ui-next';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
sizeClassName: 'w-[58px] h-[28px]',
|
||||
arrowsDirection: 'horizontal',
|
||||
labelPosition: 'bottom',
|
||||
// Helper function to safely convert any value to uppercase string
|
||||
const toUpperCaseString = value => {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
return String(value).toUpperCase();
|
||||
};
|
||||
|
||||
const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs p-2"
|
||||
>
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="text-primary-active h-[14px] w-[14px]"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Icons.ByName
|
||||
name="info-link"
|
||||
className="text-primary h-3 w-3"
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={4}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<div>{tooltip}</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||
<span className="text-muted-foreground text-xs uppercase tracking-wide">{title}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,132 +52,149 @@ const DynamicVolumeControls = ({
|
||||
minFps,
|
||||
maxFps,
|
||||
// Frames
|
||||
currentFrameIndex,
|
||||
onFrameChange,
|
||||
framesLength,
|
||||
currentDimensionGroupNumber,
|
||||
onDimensionGroupChange,
|
||||
numDimensionGroups,
|
||||
onGenerate,
|
||||
onDoubleRangeChange,
|
||||
rangeValues,
|
||||
onDynamicClick,
|
||||
}) => {
|
||||
const [computedView, setComputedView] = useState(false);
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
setSliderRangeValues(newValues);
|
||||
// Wrapper for onGenerate to handle potential errors
|
||||
const handleGenerate = () => {
|
||||
try {
|
||||
if (typeof onGenerate === 'function') {
|
||||
onGenerate(computeViewMode);
|
||||
} else {
|
||||
console.error('onGenerate is not a function', onGenerate);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in onGenerate:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatLabel = value => Math.round(value);
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
title="Controls"
|
||||
childrenClassName="space-y-4 pb-5 px-5"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<ButtonGroup className="mt-2 w-full">
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick?.();
|
||||
<PanelSection defaultOpen={true}>
|
||||
<PanelSection.Header>Controls</PanelSection.Header>
|
||||
<PanelSection.Content className="bg-muted space-y-4 px-5 pt-2 pb-4">
|
||||
<div className="mb-4">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<Tabs
|
||||
value={computedView ? 'computed' : '4d'}
|
||||
onValueChange={value => {
|
||||
const isComputed = value === 'computed';
|
||||
setComputedView(isComputed);
|
||||
if (!isComputed && typeof onDynamicClick === 'function') {
|
||||
onDynamicClick();
|
||||
}
|
||||
}}
|
||||
className="my-2 w-full"
|
||||
>
|
||||
4D
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(true);
|
||||
}}
|
||||
>
|
||||
Computed
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FrameControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
framesLength={framesLength}
|
||||
onFrameChange={onFrameChange}
|
||||
currentFrameIndex={currentFrameIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
|
||||
applied to the data set.
|
||||
<br></br> Range Slider: Choose the numeric range within which the operation will be
|
||||
performed.
|
||||
<br></br>Generate Button: Execute the chosen operation on the specified range of
|
||||
data.{' '}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full`}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="mt-2 w-full">
|
||||
<DoubleSlider
|
||||
min={0}
|
||||
max={framesLength - 1}
|
||||
step={1}
|
||||
defaultValue={sliderRangeValues}
|
||||
onValueChange={handleSliderChange}
|
||||
formatLabel={formatLabel}
|
||||
className="w-full"
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger
|
||||
value="4d"
|
||||
className="w-1/2"
|
||||
>
|
||||
4D
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="computed"
|
||||
className="w-1/2"
|
||||
>
|
||||
Computed
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div>
|
||||
<DimensionGroupControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
numDimensionGroups={numDimensionGroups}
|
||||
onDimensionGroupChange={onDimensionGroupChange}
|
||||
currentDimensionGroupNumber={currentDimensionGroupNumber}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||
onClick={() => {
|
||||
onGenerate(computeViewMode);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`mt-3 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to
|
||||
be applied to the data set.
|
||||
<br /> Range Slider: Choose the numeric range of dimension groups within which the
|
||||
operation will be performed.
|
||||
<br />
|
||||
Generate Button: Execute the chosen operation on the specified range of data.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Tabs
|
||||
value={String(computeViewMode)}
|
||||
onValueChange={value => {
|
||||
setComputeViewMode(value);
|
||||
}}
|
||||
className="mt-2 w-full"
|
||||
>
|
||||
<TabsList className="w-full gap-1">
|
||||
{' '}
|
||||
<TabsTrigger
|
||||
value={String(Enums.DynamicOperatorType.SUM)}
|
||||
className="w-1/3"
|
||||
>
|
||||
{toUpperCaseString(Enums.DynamicOperatorType.SUM)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={String(Enums.DynamicOperatorType.AVERAGE)}
|
||||
className="w-1/3"
|
||||
>
|
||||
{toUpperCaseString(Enums.DynamicOperatorType.AVERAGE)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={String(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
className="w-1/3"
|
||||
>
|
||||
{toUpperCaseString(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="mt-2 w-full">
|
||||
<Numeric.Container
|
||||
mode="doubleRange"
|
||||
min={1}
|
||||
max={numDimensionGroups || 1}
|
||||
values={rangeValues || [1, numDimensionGroups || 1]}
|
||||
onChange={onDoubleRangeChange || (() => {})}
|
||||
>
|
||||
<Numeric.DoubleRange showNumberInputs />
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="mt-2 h-[26px] w-[115px] self-start p-0"
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection.Content>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
@@ -178,58 +202,84 @@ const DynamicVolumeControls = ({
|
||||
|
||||
export default DynamicVolumeControls;
|
||||
|
||||
function FrameControls({
|
||||
function DimensionGroupControls({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
fps,
|
||||
minFps,
|
||||
maxFps,
|
||||
onFpsChange,
|
||||
framesLength,
|
||||
onFrameChange,
|
||||
currentFrameIndex,
|
||||
numDimensionGroups,
|
||||
onDimensionGroupChange,
|
||||
currentDimensionGroupNumber,
|
||||
computedView,
|
||||
}) {
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
return (
|
||||
<div className={computedView && 'ohif-disabled'}>
|
||||
<div className={computedView ? 'ohif-disabled' : ''}>
|
||||
<Header
|
||||
title="4D Controls"
|
||||
tooltip={
|
||||
<div>
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
|
||||
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
|
||||
Second) Selector: Adjust the playback speed of the animation.
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br />
|
||||
Dimension Group Selector: Navigate through individual dimension groups of the 4D data.{' '}
|
||||
<br />
|
||||
FPS (Frames Per Second) Selector: Adjust the playback speed of the animation.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="mt-3 flex justify-between">
|
||||
<IconButton
|
||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
<Button
|
||||
id="play-pause-button"
|
||||
variant="secondary"
|
||||
size="default"
|
||||
className="w-[58px]"
|
||||
onClick={() => {
|
||||
if (typeof onPlayPauseChange === 'function') {
|
||||
onPlayPauseChange(!isPlaying);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
<Icons.ByName
|
||||
name={getPlayPauseIconName()}
|
||||
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
className="text-foreground h-[24px] w-[24px]"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength - 1}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
<InputNumber
|
||||
value={fps}
|
||||
onChange={onFpsChange}
|
||||
minValue={minFps}
|
||||
maxValue={maxFps}
|
||||
{...controlClassNames}
|
||||
label="FPS"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
value={currentDimensionGroupNumber || 1}
|
||||
onChange={onDimensionGroupChange || (() => {})}
|
||||
min={1}
|
||||
max={numDimensionGroups || 1}
|
||||
step={1}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Numeric.NumberStepper
|
||||
className="h-[28px] w-[58px]"
|
||||
direction="horizontal"
|
||||
/>
|
||||
<Numeric.Label className="text-muted-foreground mt-1 text-sm">Frame</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
|
||||
<Numeric.Container
|
||||
mode="stepper"
|
||||
value={fps || 1}
|
||||
onChange={onFpsChange || (() => {})}
|
||||
min={minFps || 1}
|
||||
max={maxFps || 30}
|
||||
step={1}
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Numeric.NumberStepper
|
||||
className="h-[28px] w-[58px]"
|
||||
direction="horizontal"
|
||||
/>
|
||||
<Numeric.Label className="text-muted-foreground mt-1 text-sm">FPS</Numeric.Label>
|
||||
</div>
|
||||
</Numeric.Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import { InputDoubleRange } from '@ohif/ui';
|
||||
import { Select } from '@ohif/ui';
|
||||
import { Button } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GenerateVolume = ({
|
||||
rangeValues,
|
||||
handleSliderChange,
|
||||
operationsUI,
|
||||
options,
|
||||
handleGenerateOptionsChange,
|
||||
onGenerateImage,
|
||||
returnTo4D,
|
||||
displayingComputedVolume,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2 text-white">Computed Image</div>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mr-2 bg-black text-white "
|
||||
options={operationsUI}
|
||||
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||
value={options.Operation}
|
||||
onChange={({ value }) => {
|
||||
handleGenerateOptionsChange({
|
||||
Operation: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={rangeValues[0] || 1}
|
||||
maxValue={rangeValues[1] || 2}
|
||||
showLabel={true}
|
||||
step={1}
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={onGenerateImage}
|
||||
className="w-1/2"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={returnTo4D}
|
||||
disabled={!displayingComputedVolume}
|
||||
className="w-1/2"
|
||||
>
|
||||
Return To 4D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateVolume.propTypes = {
|
||||
rangeValues: PropTypes.array.isRequired,
|
||||
handleSliderChange: PropTypes.func.isRequired,
|
||||
operationsUI: PropTypes.array.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||
onGenerateImage: PropTypes.func.isRequired,
|
||||
returnTo4D: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GenerateVolume;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { useCine } from '@ohif/ui-next';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
@@ -14,13 +15,12 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
|
||||
const [dimensionGroupRange, setDimensionGroupRange] = useState([1, 1]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||
const [dimensionGroupNumberRendered, setDimensionGroupNumberRendered] = useState(null);
|
||||
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||
|
||||
//
|
||||
@@ -37,9 +37,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
if (volumeData.volume?.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
const newRange = [1, volumeData.volume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
const newRange = [1, volumeData.volume.numDimensionGroups];
|
||||
setDimensionGroupRange(newRange);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -64,10 +63,10 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_DIMENSION_GROUP_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
setDimensionGroupNumberRendered(evt.detail.dimensionGroupNumber);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
@@ -75,7 +74,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
@@ -101,9 +100,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
const newRange = [1, dynamicVolume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
setDimensionGroupRange([1, dynamicVolume.numDimensionGroups]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
viewportGridService,
|
||||
@@ -140,11 +137,12 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
}
|
||||
const [start, end] = timePointsRangeToUseForGenerate;
|
||||
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
|
||||
const [start, end] = dimensionGroupRange;
|
||||
// from start to end, with steps of 1
|
||||
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
|
||||
const options = {
|
||||
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
|
||||
dimensionGroupNumbers: operationName === 'SUBTRACT' ? [start, end] : frameNumbers,
|
||||
targetVolume: computedVolume,
|
||||
};
|
||||
|
||||
@@ -206,21 +204,6 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
handlePlay();
|
||||
};
|
||||
|
||||
function handleSliderChange(newValues) {
|
||||
if (
|
||||
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimePointsRangeToUseForGenerate(newValues);
|
||||
}
|
||||
|
||||
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicVolumeControls
|
||||
fps={frameRate}
|
||||
@@ -228,16 +211,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
minFps={1}
|
||||
maxFps={50}
|
||||
currentFrameIndex={timePointRendered}
|
||||
onFpsChange={handleSetFrameRate}
|
||||
framesLength={timePointsRange[1]}
|
||||
onFrameChange={timePointIndex => {
|
||||
dynamicVolume.timePointIndex = timePointIndex;
|
||||
currentDimensionGroupNumber={dimensionGroupNumberRendered}
|
||||
numDimensionGroups={dynamicVolume?.numDimensionGroups || 1}
|
||||
onDimensionGroupChange={dimensionGroupNumber => {
|
||||
dynamicVolume.dimensionGroupNumber = dimensionGroupNumber;
|
||||
}}
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
initialRangeValues={timePointsRangeToUseForGenerate}
|
||||
onDoubleRangeChange={setDimensionGroupRange}
|
||||
rangeValues={dimensionGroupRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ function WorkflowPanel({ servicesManager }: { servicesManager: ServicesManager }
|
||||
const ProgressDropdownWithService =
|
||||
servicesManager.services.customizationService.getCustomization(
|
||||
'progressDropdownWithServiceComponent'
|
||||
).component;
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.10.0-beta.12",
|
||||
"version": "3.11.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -38,11 +38,10 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^2.2.20",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@ohif/core": "3.10.0-beta.12",
|
||||
"@ohif/ui": "3.10.0-beta.12",
|
||||
"dcmjs": "*",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.32.5",
|
||||
"@ohif/core": "3.11.0",
|
||||
"@ohif/ui": "3.11.0",
|
||||
"dcmjs": "0.43.1",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -50,19 +49,22 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-resize-detector": "^10.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^2.2.20",
|
||||
"@cornerstonejs/core": "^2.2.20",
|
||||
"@cornerstonejs/tools": "^2.2.20",
|
||||
"@icr/polyseg-wasm": "^0.4.0",
|
||||
"@kitware/vtk.js": "32.1.1",
|
||||
"@cornerstonejs/adapters": "^3.32.5",
|
||||
"@cornerstonejs/ai": "^3.32.5",
|
||||
"@cornerstonejs/core": "^3.32.5",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.32.5",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.32.5",
|
||||
"@cornerstonejs/tools": "^3.32.5",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.compact": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.zip": "^4.2.0",
|
||||
"shader-loader": "^1.3.1",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
width: 100%;
|
||||
height: 100%; /* MUST have `height` to prevent resize infinite loop */
|
||||
position: relative;
|
||||
/* Prevent text selection on the entire viewport wrapper */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.cornerstone-viewport-element {
|
||||
@@ -16,4 +21,26 @@
|
||||
/* Prevents the entire page from getting larger
|
||||
when the magnify tool is near the sides/corners of the page */
|
||||
overflow: hidden;
|
||||
|
||||
/* Prevent text selection on the viewport element */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Prevent text selection on overlay elements */
|
||||
.noselect {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Ensure all elements within viewport wrapper prevent text selection */
|
||||
.viewport-wrapper * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import * as cs3DTools from '@cornerstonejs/tools';
|
||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import { AllInOneMenu, Notification, useViewportDialog } from '@ohif/ui';
|
||||
import { MeasurementService, useViewportRef } from '@ohif/core';
|
||||
import { useViewportDialog } from '@ohif/ui-next';
|
||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
@@ -14,20 +13,15 @@ import CinePlayer from '../components/CinePlayer';
|
||||
import type { Types } from '@ohif/core';
|
||||
|
||||
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
|
||||
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
|
||||
import { useAppConfig } from '@state';
|
||||
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
|
||||
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
|
||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
/**
|
||||
* Caches the jump to measurement operation, so that if display set is shown,
|
||||
* it can jump to the measurement.
|
||||
*/
|
||||
let cacheJumpToMeasurementEvent;
|
||||
// Cache for viewport dimensions, persists across component remounts
|
||||
const viewportDimensions = new Map<string, { width: number; height: number }>();
|
||||
|
||||
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||
// Then we don't need to worry about the re-renders if the props change.
|
||||
@@ -46,7 +40,6 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
onElementDisabled,
|
||||
@@ -88,7 +81,7 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
const [appConfig] = useAppConfig();
|
||||
const viewportRef = useViewportRef(viewportId);
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
@@ -98,23 +91,56 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
cornerstoneViewportService,
|
||||
segmentationService,
|
||||
cornerstoneCacheService,
|
||||
viewportActionCornersService,
|
||||
customizationService,
|
||||
measurementService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
// useCallback for scroll bar height calculation
|
||||
const setImageScrollBarHeight = useCallback(() => {
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 10}px`;
|
||||
setScrollbarHeight(scrollbarHeight);
|
||||
}, [elementRef]);
|
||||
|
||||
// useCallback for onResize
|
||||
const onResize = useCallback(() => {
|
||||
if (elementRef.current) {
|
||||
cornerstoneViewportService.resize();
|
||||
setImageScrollBarHeight();
|
||||
const onResize = useCallback(
|
||||
(entries: ResizeObserverEntry[]) => {
|
||||
if (elementRef.current && entries?.length) {
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
|
||||
const prevDimensions = viewportDimensions.get(viewportId) || { width: 0, height: 0 };
|
||||
|
||||
// Check if dimensions actually changed and then only resize if they have changed
|
||||
const hasDimensionsChanged =
|
||||
prevDimensions.width !== width || prevDimensions.height !== height;
|
||||
|
||||
if (width > 0 && height > 0 && hasDimensionsChanged) {
|
||||
viewportDimensions.set(viewportId, { width, height });
|
||||
// Perform resize operations
|
||||
cornerstoneViewportService.resize();
|
||||
setImageScrollBarHeight();
|
||||
}
|
||||
}
|
||||
},
|
||||
[viewportId, elementRef, cornerstoneViewportService, setImageScrollBarHeight]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
}, [elementRef]);
|
||||
|
||||
const resizeObserver = new ResizeObserver(onResize);
|
||||
resizeObserver.observe(element);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
resizeObserver.unobserve(element);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [onResize]);
|
||||
|
||||
const cleanUpServices = useCallback(
|
||||
viewportInfo => {
|
||||
@@ -125,16 +151,8 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||
|
||||
segmentationService.clearSegmentationRepresentations(viewportId);
|
||||
|
||||
viewportActionCornersService.clear(viewportId);
|
||||
},
|
||||
[
|
||||
viewportId,
|
||||
segmentationService,
|
||||
syncGroupService,
|
||||
toolGroupService,
|
||||
viewportActionCornersService,
|
||||
]
|
||||
[viewportId, segmentationService, syncGroupService, toolGroupService]
|
||||
);
|
||||
|
||||
const elementEnabledHandler = useCallback(
|
||||
@@ -146,6 +164,11 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
|
||||
const { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEnabledElement(viewportId, element);
|
||||
setEnabledVPElement(element);
|
||||
|
||||
@@ -197,6 +220,7 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
}
|
||||
|
||||
cornerstoneViewportService.disableElement(viewportId);
|
||||
viewportRef.unregister();
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||
};
|
||||
@@ -258,14 +282,6 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
|
||||
const presentations = getViewportPresentations(viewportId, viewportOptions);
|
||||
|
||||
let measurement;
|
||||
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
|
||||
measurement = cacheJumpToMeasurementEvent.measurement;
|
||||
// Delete the position presentation so that viewport navigates direct
|
||||
presentations.positionPresentation = null;
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
}
|
||||
|
||||
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
||||
// Used for segmentation hydration right now, since the logic to decide whether
|
||||
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
||||
@@ -283,95 +299,12 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
displaySetOptions,
|
||||
presentations
|
||||
);
|
||||
|
||||
if (measurement) {
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
}
|
||||
};
|
||||
|
||||
loadViewportData();
|
||||
}, [viewportOptions, displaySets, dataSource]);
|
||||
|
||||
/**
|
||||
* There are two scenarios for jump to click
|
||||
* 1. Current viewports contain the displaySet that the annotation was drawn on
|
||||
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
|
||||
* and we need to change the viewports displaySet for jumping.
|
||||
* Since measurement_jump happens via events and listeners, the former case is handled
|
||||
* by the measurement_jump direct callback, but the latter case is handled first by
|
||||
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
|
||||
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isJumpToMeasurementDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
elementRef,
|
||||
viewportId,
|
||||
servicesManager
|
||||
);
|
||||
|
||||
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
|
||||
|
||||
// Set up the window level action menu in the viewport action corners.
|
||||
useEffect(() => {
|
||||
// Doing an === check here because the default config value when not set is true
|
||||
if (appConfig.addWindowLevelActionMenu === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = viewportActionCornersService.LOCATIONS.topRight;
|
||||
|
||||
// TODO: In the future we should consider using the customization service
|
||||
// to determine if and in which corner various action components should go.
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: 'windowLevelActionMenu',
|
||||
component: getWindowLevelActionMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
location,
|
||||
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
|
||||
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
|
||||
}),
|
||||
location,
|
||||
});
|
||||
|
||||
viewportActionCornersService.addComponent({
|
||||
viewportId,
|
||||
id: 'segmentation',
|
||||
component: getViewportDataOverlaySettingsMenu({
|
||||
viewportId,
|
||||
element: elementRef.current,
|
||||
displaySets,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
location,
|
||||
}),
|
||||
location,
|
||||
});
|
||||
}, [
|
||||
displaySets,
|
||||
viewportId,
|
||||
viewportActionCornersService,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
]);
|
||||
|
||||
const { ref: resizeRef } = useResizeDetector({
|
||||
onResize,
|
||||
});
|
||||
const Notification = customizationService.getCustomization('ui.notificationComponent');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -381,9 +314,12 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
data-viewportid={viewportId}
|
||||
ref={el => {
|
||||
resizeRef.current = el;
|
||||
elementRef.current = el;
|
||||
if (el) {
|
||||
viewportRef.register(el);
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
<CornerstoneOverlays
|
||||
@@ -425,97 +361,6 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
areEqual
|
||||
);
|
||||
|
||||
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
|
||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||
props => {
|
||||
cacheJumpToMeasurementEvent = props;
|
||||
const { viewportId: jumpId, measurement, isConsumed } = props;
|
||||
if (!measurement || isConsumed) {
|
||||
return;
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
|
||||
// Decide on which viewport should handle this
|
||||
cacheJumpToMeasurementEvent.cornerstoneViewport =
|
||||
cornerstoneViewportService.getViewportIdToJump(jumpId, {
|
||||
displaySetInstanceUID: measurement.displaySetInstanceUID,
|
||||
...measurement.metadata,
|
||||
referencedImageId:
|
||||
measurement.referencedImageId || measurement.metadata?.referencedImageId,
|
||||
});
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||
return;
|
||||
}
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
);
|
||||
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
// Check if there is a queued jumpToMeasurement event
|
||||
function _checkForCachedJumpToMeasurementEvents(
|
||||
elementRef,
|
||||
viewportId,
|
||||
displaySets,
|
||||
servicesManager
|
||||
) {
|
||||
if (!cacheJumpToMeasurementEvent) {
|
||||
return;
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.isConsumed) {
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
return;
|
||||
}
|
||||
const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
|
||||
if (!displaysUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Jump to measurement if the measurement exists
|
||||
const { measurement } = cacheJumpToMeasurementEvent;
|
||||
if (measurement && elementRef) {
|
||||
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||
_jumpToMeasurement(measurement, elementRef, viewportId, servicesManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesManager) {
|
||||
const { viewportGridService } = servicesManager.services;
|
||||
|
||||
const targetElement = targetElementRef.current;
|
||||
|
||||
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||
// to set it properly
|
||||
// setCornerstoneMeasurementActive(measurement);
|
||||
|
||||
viewportGridService.setActiveViewportId(viewportId);
|
||||
|
||||
const enabledElement = getEnabledElement(targetElement);
|
||||
|
||||
if (enabledElement) {
|
||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
|
||||
|
||||
const { metadata } = measurement;
|
||||
if (!viewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })) {
|
||||
console.log("Reference isn't viewable, postponing until updated");
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.setViewReference(metadata);
|
||||
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
// Jump to measurement consumed, remove.
|
||||
cacheJumpToMeasurementEvent?.consume?.();
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
|
||||
const { synchronizersStore } = useSynchronizersStore.getState();
|
||||
const synchronizers = synchronizersStore[viewportId];
|
||||
@@ -576,22 +421,18 @@ OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||
|
||||
function areEqual(prevProps, nextProps) {
|
||||
if (nextProps.needsRerendering) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -599,12 +440,10 @@ function areEqual(prevProps, nextProps) {
|
||||
nextProps.viewportOptions.viewportType &&
|
||||
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
|
||||
) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextProps.viewportOptions.needsRerendering) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -612,7 +451,6 @@ function areEqual(prevProps, nextProps) {
|
||||
const nextDisplaySets = nextProps.displaySets;
|
||||
|
||||
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -625,13 +463,11 @@ function areEqual(prevProps, nextProps) {
|
||||
);
|
||||
|
||||
if (!foundDisplaySet) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// check they contain the same image
|
||||
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -639,7 +475,6 @@ function areEqual(prevProps, nextProps) {
|
||||
if (foundDisplaySet.images?.length) {
|
||||
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
||||
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
|
||||
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -649,16 +484,4 @@ function areEqual(prevProps, nextProps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function to check if display sets have changed
|
||||
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
|
||||
if (prevDisplaySets.length !== currentDisplaySets.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return currentDisplaySets.some((currentDS, index) => {
|
||||
const prevDS = prevDisplaySets[index];
|
||||
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
|
||||
});
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneViewport;
|
||||
@@ -1,16 +1,20 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { metaData, Enums, utilities, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
|
||||
import type { ImageSliceData } from '@cornerstonejs/core/types';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import { ViewportOverlay } from '@ohif/ui-next';
|
||||
import type { InstanceMetadata } from '@ohif/core/src/types';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { utils } from '@ohif/core';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
import './CustomizableViewportOverlay.css';
|
||||
import { useViewportRendering } from '../../hooks';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
const { formatPN } = utils;
|
||||
|
||||
type ViewportData = StackViewportData | VolumeViewportData;
|
||||
|
||||
@@ -45,80 +49,6 @@ const OverlayItemComponents = {
|
||||
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
|
||||
};
|
||||
|
||||
const studyDateItem = {
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
|
||||
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
|
||||
formatDate(referenceInstance.StudyDate),
|
||||
};
|
||||
|
||||
const seriesDescriptionItem = {
|
||||
id: 'SeriesDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Series description',
|
||||
condition: ({ referenceInstance }) => {
|
||||
return referenceInstance && referenceInstance.SeriesDescription;
|
||||
},
|
||||
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
|
||||
};
|
||||
|
||||
const topLeftItems = {
|
||||
id: 'cornerstoneOverlayTopLeft',
|
||||
items: [studyDateItem, seriesDescriptionItem],
|
||||
};
|
||||
|
||||
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
|
||||
|
||||
const bottomLeftItems = {
|
||||
id: 'cornerstoneOverlayBottomLeft',
|
||||
items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'ZoomLevel',
|
||||
customizationType: 'ohif.overlayItem.zoomLevel',
|
||||
condition: props => {
|
||||
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
|
||||
return activeToolName === 'Zoom';
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const bottomRightItems = {
|
||||
id: 'cornerstoneOverlayBottomRight',
|
||||
items: [
|
||||
{
|
||||
id: 'InstanceNumber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* The @ohif/cornerstoneOverlay is a default value for a customization
|
||||
* for the cornerstone overlays. The intent is to allow it to be extended
|
||||
* without needing to re-write the individual overlays by using the append
|
||||
* mechanism. Individual attributes can be modified individually without
|
||||
* affecting the other items by using the append as well, with position
|
||||
* based replacement.
|
||||
* This is used as a default in the getCustomizationModule so that it
|
||||
* is available early for additional customization extensions.
|
||||
*/
|
||||
const CornerstoneOverlay = {
|
||||
id: '@ohif/cornerstoneOverlay',
|
||||
topLeftItems,
|
||||
topRightItems,
|
||||
bottomLeftItems,
|
||||
bottomRightItems,
|
||||
};
|
||||
|
||||
/**
|
||||
* Customizable Viewport Overlay
|
||||
*/
|
||||
@@ -139,28 +69,22 @@ function CustomizableViewportOverlay({
|
||||
servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [annotationState, setAnnotationState] = useState(0);
|
||||
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
|
||||
const { imageIndex } = imageSliceData;
|
||||
|
||||
// The new customization is 'cornerstoneOverlay', with an append or replace
|
||||
// on the individual items rather than defining individual items.
|
||||
const cornerstoneOverlay = customizationService.getCustomization('@ohif/cornerstoneOverlay');
|
||||
|
||||
// Historical usage defined the overlays as separate items due to lack of
|
||||
// append functionality. This code enables the historical usage, but
|
||||
// the recommended functionality is to append to the default values in
|
||||
// cornerstoneOverlay rather than defining individual items.
|
||||
const topLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
|
||||
cornerstoneOverlay?.topLeftItems;
|
||||
const topRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
|
||||
cornerstoneOverlay?.topRightItems;
|
||||
const bottomLeftCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
|
||||
cornerstoneOverlay?.bottomLeftItems;
|
||||
const bottomRightCustomization =
|
||||
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
|
||||
cornerstoneOverlay?.bottomRightItems;
|
||||
const topLeftCustomization = customizationService.getCustomization('viewportOverlay.topLeft');
|
||||
const topRightCustomization = customizationService.getCustomization('viewportOverlay.topRight');
|
||||
const bottomLeftCustomization = customizationService.getCustomization(
|
||||
'viewportOverlay.bottomLeft'
|
||||
);
|
||||
const bottomRightCustomization = customizationService.getCustomization(
|
||||
'viewportOverlay.bottomRight'
|
||||
);
|
||||
|
||||
const instanceNumber = useMemo(
|
||||
() =>
|
||||
@@ -210,6 +134,20 @@ function CustomizableViewportOverlay({
|
||||
};
|
||||
}, [viewportId, viewportData, voi, element]);
|
||||
|
||||
const annotationModified = useCallback(evt => {
|
||||
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
|
||||
// Update the annotation state to trigger a re-render
|
||||
setAnnotationState(prevState => prevState + 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(csToolsEnums.Events.ANNOTATION_MODIFIED, annotationModified);
|
||||
};
|
||||
}, [annotationModified]);
|
||||
/**
|
||||
* Updating the scale when the viewport changes its zoom
|
||||
*/
|
||||
@@ -250,6 +188,7 @@ function CustomizableViewportOverlay({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
customization: item,
|
||||
isLight,
|
||||
formatters: {
|
||||
formatPN,
|
||||
formatDate: formatDICOMDate,
|
||||
@@ -262,8 +201,8 @@ function CustomizableViewportOverlay({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { customizationType } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[customizationType];
|
||||
const { inheritsFrom } = item;
|
||||
const OverlayItemComponent = OverlayItemComponents[inheritsFrom];
|
||||
|
||||
if (OverlayItemComponent) {
|
||||
return <OverlayItemComponent {...overlayItemProps} />;
|
||||
@@ -286,15 +225,12 @@ function CustomizableViewportOverlay({
|
||||
voi,
|
||||
scale,
|
||||
instanceNumber,
|
||||
annotationState,
|
||||
]
|
||||
);
|
||||
|
||||
const getContent = useCallback(
|
||||
(customization, keyPrefix) => {
|
||||
if (!customization?.items) {
|
||||
return null;
|
||||
}
|
||||
const { items } = customization;
|
||||
const props = {
|
||||
...displaySetProps,
|
||||
formatters: { formatDate: formatDICOMDate },
|
||||
@@ -303,11 +239,12 @@ function CustomizableViewportOverlay({
|
||||
instanceNumber,
|
||||
viewportId,
|
||||
toolGroupService,
|
||||
isLight,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, index) => (
|
||||
{customization.map((item, index) => (
|
||||
<div key={`${keyPrefix}_${index}`}>
|
||||
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
|
||||
null}
|
||||
@@ -325,6 +262,8 @@ function CustomizableViewportOverlay({
|
||||
topRight={getContent(topRightCustomization, 'topRightOverlayItem')}
|
||||
bottomLeft={getContent(bottomLeftCustomization, 'bottomLeftOverlayItem')}
|
||||
bottomRight={getContent(bottomRightCustomization, 'bottomRightOverlayItem')}
|
||||
color={isLight ? 'text-neutral-dark' : 'text-neutral-light'}
|
||||
shadowClass={isLight ? 'shadow-light' : 'shadow-dark'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -452,13 +391,14 @@ function OverlayItem(props) {
|
||||
title={title}
|
||||
>
|
||||
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
|
||||
<span className="ml-1 mr-2 shrink-0">{value}</span>
|
||||
<span className="ml-0 mr-2 shrink-0">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Window Level / Center Overlay item
|
||||
* //
|
||||
*/
|
||||
function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
@@ -471,10 +411,10 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: customization?.color }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 shrink-0">{windowCenter.toFixed(0)}</span>
|
||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
|
||||
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">L:</span>
|
||||
<span className="shrink-0">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -488,7 +428,7 @@ function ZoomOverlayItem({ scale, customization }: OverlayItemProps) {
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">Zoom:</span>
|
||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">Zoom:</span>
|
||||
<span>{scale.toFixed(2)}x</span>
|
||||
</div>
|
||||
);
|
||||
@@ -512,7 +452,7 @@ function InstanceNumberOverlayItem({
|
||||
<span>
|
||||
{instanceNumber !== undefined && instanceNumber !== null ? (
|
||||
<>
|
||||
<span className="mr-1 shrink-0">I:</span>
|
||||
<span className="mr-0.5 shrink-0 opacity-[0.70]">I:</span>
|
||||
<span>{`${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`}</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -531,4 +471,4 @@ CustomizableViewportOverlay.propTypes = {
|
||||
|
||||
export default CustomizableViewportOverlay;
|
||||
|
||||
export { CustomizableViewportOverlay, CornerstoneOverlay };
|
||||
export { CustomizableViewportOverlay };
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { ImageScrollbar } from '@ohif/ui';
|
||||
import { ImageScrollbar } from '@ohif/ui-next';
|
||||
|
||||
function CornerstoneImageScrollbar({
|
||||
viewportData,
|
||||
@@ -44,13 +44,17 @@ function CornerstoneImageScrollbar({
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
try {
|
||||
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -59,7 +63,7 @@ function CornerstoneImageScrollbar({
|
||||
}
|
||||
const { viewportType } = viewportData;
|
||||
const eventId =
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_VIEWPORT_SCROLL) ||
|
||||
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
|
||||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
|
||||
Enums.Events.IMAGE_RENDERED;
|
||||
|
||||
@@ -68,11 +72,11 @@ function CornerstoneImageScrollbar({
|
||||
if (!viewport || viewport instanceof VolumeViewport3D) {
|
||||
return;
|
||||
}
|
||||
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
|
||||
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
|
||||
const numberOfSlices = viewport.getNumberOfSlices();
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
imageIndex: newImageIdIndex ?? imageIdIndex,
|
||||
numberOfSlices,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,27 +3,30 @@
|
||||
--marker-height: 100px;
|
||||
--scrollbar-width: 20px;
|
||||
pointer-events: none;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.ViewportOrientationMarkers .orientation-marker {
|
||||
position: absolute;
|
||||
}
|
||||
.ViewportOrientationMarkers .top-mid {
|
||||
top: 0.6rem;
|
||||
top: 0.38rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.ViewportOrientationMarkers .left-mid {
|
||||
top: 47%;
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
left: 0.38rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.ViewportOrientationMarkers .right-mid {
|
||||
top: 47%;
|
||||
top: 50%;
|
||||
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.ViewportOrientationMarkers .bottom-mid {
|
||||
top: calc(100% - var(--marker-height) - 0.6rem);
|
||||
left: 47%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
||||
display: flex;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
|
||||
import {
|
||||
metaData,
|
||||
Enums,
|
||||
Types,
|
||||
getEnabledElement,
|
||||
utilities as coreUtilities,
|
||||
} from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import './ViewportOrientationMarkers.css';
|
||||
|
||||
import { useViewportRendering } from '../../hooks';
|
||||
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
|
||||
|
||||
function ViewportOrientationMarkers({
|
||||
@@ -20,8 +26,34 @@ function ViewportOrientationMarkers({
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [flipHorizontal, setFlipHorizontal] = useState(false);
|
||||
const [flipVertical, setFlipVertical] = useState(false);
|
||||
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId);
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
// Store initial viewUp and viewRight for volume viewports
|
||||
const initialVolumeOrientationRef = useRef<{
|
||||
initialViewUp: number[] | null;
|
||||
initialViewRight: number[] | null;
|
||||
}>({
|
||||
initialViewUp: null,
|
||||
initialViewRight: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initialVolumeOrientationRef.current.initialViewUp = null;
|
||||
initialVolumeOrientationRef.current.initialViewRight = null;
|
||||
|
||||
if (viewportData?.viewportType !== 'stack' && element && getEnabledElement(element)) {
|
||||
const { viewport } = getEnabledElement(element);
|
||||
const { viewUp, viewPlaneNormal } = viewport.getCamera();
|
||||
|
||||
const viewRight = vec3.create();
|
||||
vec3.cross(viewRight, viewUp, viewPlaneNormal);
|
||||
|
||||
initialVolumeOrientationRef.current.initialViewUp = [...viewUp];
|
||||
initialVolumeOrientationRef.current.initialViewRight = [...viewRight];
|
||||
}
|
||||
}, [element, viewportData]);
|
||||
|
||||
useEffect(() => {
|
||||
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
|
||||
const { previousCamera, camera } = evt.detail;
|
||||
@@ -79,14 +111,21 @@ function ViewportOrientationMarkers({
|
||||
return '';
|
||||
}
|
||||
|
||||
const { viewport } = getEnabledElement(element);
|
||||
const { viewUp, viewPlaneNormal } = viewport.getCamera();
|
||||
|
||||
const viewRight = vec3.create();
|
||||
vec3.cross(viewRight, viewUp, viewPlaneNormal);
|
||||
|
||||
columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]];
|
||||
rowCosines = viewRight;
|
||||
if (
|
||||
initialVolumeOrientationRef.current.initialViewUp &&
|
||||
initialVolumeOrientationRef.current.initialViewRight
|
||||
) {
|
||||
// Use initial orientation values for consistency, even as the camera changes
|
||||
columnCosines = [
|
||||
-initialVolumeOrientationRef.current.initialViewUp[0],
|
||||
-initialVolumeOrientationRef.current.initialViewUp[1],
|
||||
-initialVolumeOrientationRef.current.initialViewUp[2],
|
||||
];
|
||||
rowCosines = initialVolumeOrientationRef.current.initialViewRight;
|
||||
} else {
|
||||
console.warn('ViewportOrientationMarkers::No initial orientation values');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -119,8 +158,9 @@ function ViewportOrientationMarkers({
|
||||
className={classNames(
|
||||
'overlay-text',
|
||||
`${m}-mid orientation-marker`,
|
||||
'text-aqua-pale',
|
||||
'text-[13px]',
|
||||
isLight ? 'text-neutral-dark/70' : 'text-neutral-light/70',
|
||||
isLight ? 'shadow-light' : 'shadow-dark',
|
||||
'text-base',
|
||||
'leading-5'
|
||||
)}
|
||||
key={`${m}-mid orientation-marker`}
|
||||
@@ -136,6 +176,7 @@ function ViewportOrientationMarkers({
|
||||
flipHorizontal,
|
||||
orientationMarkers,
|
||||
element,
|
||||
isLight,
|
||||
]);
|
||||
|
||||
return <div className="ViewportOrientationMarkers select-none">{markers}</div>;
|
||||
@@ -157,6 +198,9 @@ function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertica
|
||||
const oppositeRowString = invertOrientationStringLPS(rowString);
|
||||
const oppositeColumnString = invertOrientationStringLPS(columnString);
|
||||
|
||||
// Round to 4 decimal places (after rotate right 3 times and flip horizontally -> rotation is 90.00000000000001)
|
||||
rotation = Math.round(rotation * 10000) / 10000;
|
||||
|
||||
const markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
|
||||
@@ -52,31 +52,6 @@ export function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
|
||||
return moment(time, 'HH:mm:ss').format(strFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a patient name for display purposes
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {string} formatted name.
|
||||
*/
|
||||
export function formatPN(name) {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
if (typeof name === 'object') {
|
||||
name = name.Alphabetic;
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const cleaned = name
|
||||
.split('^')
|
||||
.filter(s => !!s)
|
||||
.join(', ')
|
||||
.trim();
|
||||
return cleaned === ',' || cleaned === '' ? '' : cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets compression type
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
|
||||
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
||||
|
||||
export type AccordionGrouping = {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
|
||||
export type AccordionGroupProps = {
|
||||
grouping: AccordionGrouping;
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for the required type from the provided allChildren list and
|
||||
* renders them.
|
||||
*/
|
||||
export const CloneChildren = props => {
|
||||
const { group, allChildren, children, childType, defaultTypes } = props;
|
||||
|
||||
const subType = group?.subType;
|
||||
|
||||
for (const child of allChildren) {
|
||||
if (subType && child.props?.subType !== subType) {
|
||||
continue;
|
||||
}
|
||||
if (childType && child.type === childType) {
|
||||
return React.cloneElement(child, { ...props, children: child.props.children });
|
||||
}
|
||||
if (defaultTypes?.indexOf(child.type) === -1) {
|
||||
return childType({ ...props, children: child });
|
||||
}
|
||||
}
|
||||
|
||||
if (!children) {
|
||||
throw new Error(`No children defined for ${props.name} CloneChildren in group ${group?.name}`);
|
||||
}
|
||||
return React.cloneElement(children, props);
|
||||
};
|
||||
|
||||
/** Used to exclude defaults */
|
||||
const DEFAULT_TYPES = [GroupAccordion, Content, Trigger];
|
||||
|
||||
/**
|
||||
* An AccordionGroup is a component that splits a set of items into different
|
||||
* groups according to a set of grouping rules. It then puts the groups
|
||||
* into a set of accordion folds selected from the body of the accordion group,
|
||||
* looking for matching trigger/content sections according to the type definition
|
||||
* in the group with first one found being used.
|
||||
*
|
||||
* This design allows for easy customization of the component by declaring grouping
|
||||
* functions with default grouping setups and then only overriding the specific
|
||||
* children needing to be changed. See the PanelMeasurement for some example
|
||||
* possibilities of how to modify the default grouping, or the test-extension
|
||||
* measurements panel for a practical, working example.
|
||||
*/
|
||||
export function AccordionGroup(props) {
|
||||
const { grouping, items, children, sourceChildren, type } = props;
|
||||
const childProps = useSystem();
|
||||
let defaultValue = props.defaultValue;
|
||||
const groups = grouping.groupingFunction(items, grouping, childProps);
|
||||
|
||||
if (!defaultValue) {
|
||||
const defaultGroup = groups.values().find(group => group.isSelected);
|
||||
defaultValue = defaultGroup?.key || defaultGroup?.title;
|
||||
}
|
||||
|
||||
const valueArr =
|
||||
(Array.isArray(defaultValue) && defaultValue) || (defaultValue && [defaultValue]) || [];
|
||||
const sourceChildrenArr = sourceChildren ? React.Children.toArray(sourceChildren) : [];
|
||||
const childrenArr = children ? React.Children.toArray(children) : [];
|
||||
const allChildren = sourceChildrenArr.concat(childrenArr);
|
||||
|
||||
return (
|
||||
<CloneChildren
|
||||
allChildren={allChildren}
|
||||
groups={groups}
|
||||
childType={GroupAccordion}
|
||||
grouping={grouping}
|
||||
defaultValue={valueArr}
|
||||
name={'grouping ' + grouping.name}
|
||||
>
|
||||
<DefaultAccordion name="DefaultAccordion" />
|
||||
</CloneChildren>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultAccordion(props) {
|
||||
const { groups, defaultValue, grouping, allChildren, asChild } = props;
|
||||
if (!allChildren || !groups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Boolean(asChild)) {
|
||||
return React.cloneElement(props.children, props);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type={grouping.type || 'multiple'}
|
||||
className="text-white"
|
||||
defaultValue={defaultValue}
|
||||
>
|
||||
{[...groups.entries()].map(([key, group]) => {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={group.key + '-i'}
|
||||
value={group.key}
|
||||
>
|
||||
<CloneChildren
|
||||
allChildren={allChildren}
|
||||
group={group}
|
||||
childType={Trigger}
|
||||
name="AccordionGroup.Trigger"
|
||||
/>
|
||||
<CloneChildren
|
||||
allChildren={allChildren}
|
||||
group={group}
|
||||
childType={Content}
|
||||
defaultTypes={DEFAULT_TYPES}
|
||||
name="AccordionGroup.Content"
|
||||
/>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupAccordion(props) {
|
||||
const { groups, children } = props;
|
||||
if (!groups) {
|
||||
return null;
|
||||
}
|
||||
return [...groups.values()].map(group =>
|
||||
React.cloneElement(children, {
|
||||
...props,
|
||||
children: children.props.children,
|
||||
group,
|
||||
...group,
|
||||
key: group.title,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function Content(props) {
|
||||
const { children, asChild, ...childProps } = props;
|
||||
const { group } = props;
|
||||
Object.assign(childProps, group);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
if (asChild) {
|
||||
return React.cloneElement(children, { ...group, ...props, children: children.props.children });
|
||||
}
|
||||
return (
|
||||
<AccordionContent>
|
||||
{React.cloneElement(children, { ...group, ...props, children: children.props.children })}
|
||||
</AccordionContent>
|
||||
);
|
||||
}
|
||||
|
||||
function Trigger(props) {
|
||||
const { children, asChild, ...childProps } = props;
|
||||
const { group } = props;
|
||||
Object.assign(childProps, group);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
if (asChild) {
|
||||
return React.cloneElement(children, childProps);
|
||||
}
|
||||
return (
|
||||
<AccordionTrigger
|
||||
value={group.value}
|
||||
asChild={true}
|
||||
>
|
||||
<div>
|
||||
{React.cloneElement(children, childProps)}
|
||||
<ChevronDownIcon
|
||||
key="chevronDown"
|
||||
className="text-primary h-4 w-4 shrink-0 transition-transform duration-200"
|
||||
/>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
AccordionGroup.Content = Content;
|
||||
AccordionGroup.Trigger = Trigger;
|
||||
AccordionGroup.Accordion = GroupAccordion;
|
||||
|
||||
export default AccordionGroup;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './AccordionGroup';
|
||||
import AccordionGroup from './AccordionGroup';
|
||||
|
||||
export default AccordionGroup;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useViewportGrid } from '@ohif/ui-next';
|
||||
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
|
||||
|
||||
const ActiveViewportWindowLevel = ({ servicesManager }: withAppTypes): ReactElement => {
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { useToolbar, useViewportMousePosition } from '@ohif/core/src/hooks';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useViewportRendering } from '../../hooks';
|
||||
import { ButtonLocation } from '@ohif/core/src/services/ToolBarService/ToolbarService';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const mouseNearControlsRanges = {
|
||||
[ButtonLocation.TopMiddle]: { minX: 0, minY: 0, maxX: 1, maxY: 0.1 },
|
||||
[ButtonLocation.BottomMiddle]: { minX: 0, minY: 0.9, maxX: 1, maxY: 1 },
|
||||
[ButtonLocation.LeftMiddle]: { minX: 0, minY: 0, maxX: 0.1, maxY: 1 },
|
||||
[ButtonLocation.RightMiddle]: { minX: 0.9, minY: 0, maxX: 1, maxY: 1 },
|
||||
};
|
||||
|
||||
const getFlexDirectionClassName = (location: ButtonLocation) =>
|
||||
location === ButtonLocation.LeftMiddle || location === ButtonLocation.RightMiddle
|
||||
? 'flex-col'
|
||||
: 'flex-row';
|
||||
|
||||
function AdvancedRenderingControls({
|
||||
viewportId,
|
||||
location,
|
||||
buttonSection,
|
||||
}: {
|
||||
viewportId: string;
|
||||
location: number;
|
||||
buttonSection: string;
|
||||
}) {
|
||||
const {
|
||||
onInteraction,
|
||||
toolbarButtons,
|
||||
isItemOpen,
|
||||
isItemLocked,
|
||||
openItem,
|
||||
closeItem,
|
||||
toggleLock,
|
||||
} = useToolbar({
|
||||
buttonSection,
|
||||
});
|
||||
|
||||
const mousePosition = useViewportMousePosition(viewportId);
|
||||
const [isMouseNearControls, setIsMouseNearControls] = useState(false);
|
||||
const [showAllIcons, setShowAllIcons] = useState(true);
|
||||
const firstMountRef = useRef(true);
|
||||
const { hasColorbar } = useViewportRendering(viewportId);
|
||||
const [isAnItemOpen, setIsAnItemOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstMountRef.current) {
|
||||
firstMountRef.current = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setShowAllIcons(false);
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showAllIcons && mousePosition.isInViewport) {
|
||||
const mouseHoverLocation = mouseNearControlsRanges[location];
|
||||
if (mousePosition.isWithinNormalizedBox(mouseHoverLocation)) {
|
||||
setIsMouseNearControls(true);
|
||||
} else {
|
||||
setIsMouseNearControls(false);
|
||||
}
|
||||
}
|
||||
}, [location, mousePosition, showAllIcons]);
|
||||
|
||||
const handleOnItemOpen = useCallback(
|
||||
(id, viewportId) => {
|
||||
openItem(id, viewportId);
|
||||
setIsAnItemOpen(true);
|
||||
},
|
||||
[openItem, setIsAnItemOpen]
|
||||
);
|
||||
|
||||
const handleOnItemClose = useCallback(
|
||||
(id, viewportId) => {
|
||||
closeItem(id, viewportId);
|
||||
setIsAnItemOpen(false);
|
||||
},
|
||||
[closeItem, setIsAnItemOpen]
|
||||
);
|
||||
|
||||
if (!toolbarButtons?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasColorbar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('flex gap-2', getFlexDirectionClassName(location))}>
|
||||
{toolbarButtons.map(toolDef => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
|
||||
// Enhanced props with state and actions - respecting viewport specificity
|
||||
const enhancedProps = {
|
||||
...componentProps,
|
||||
isOpen: isItemOpen(id, viewportId),
|
||||
isLocked: isItemLocked(id, viewportId),
|
||||
onOpen: () => handleOnItemOpen(id, viewportId),
|
||||
onClose: () => handleOnItemClose(id, viewportId),
|
||||
onToggleLock: () => toggleLock(id, viewportId),
|
||||
viewportId,
|
||||
};
|
||||
|
||||
const tool = (
|
||||
<Component
|
||||
key={id}
|
||||
id={id}
|
||||
location={location}
|
||||
onInteraction={args => {
|
||||
onInteraction({
|
||||
...args,
|
||||
itemId: id,
|
||||
viewportId,
|
||||
});
|
||||
}}
|
||||
{...enhancedProps}
|
||||
/>
|
||||
);
|
||||
|
||||
// Always show all icons on first mount for 3 seconds
|
||||
// After that, always show Colorbar, show others only when mouse is at bottom
|
||||
const shouldBeVisible =
|
||||
isAnItemOpen || showAllIcons || id === 'Colorbar' || isMouseNearControls;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={shouldBeVisible ? 'opacity-100' : 'pointer-events-none opacity-0'}
|
||||
style={{ transition: 'opacity 0.2s ease-in-out' }}
|
||||
>
|
||||
{tool}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedRenderingControls;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './AdvancedRenderingControls';
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { useCine } from '@ohif/ui-next';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
@@ -53,13 +53,13 @@ function WrappedCinePlayer({
|
||||
// check if the displaySet is dynamic and set the dynamic info
|
||||
if (displaySet.isDynamicVolume) {
|
||||
const { dynamicVolumeInfo } = displaySet;
|
||||
const numTimePoints = dynamicVolumeInfo.timePoints.length;
|
||||
const numDimensionGroups = dynamicVolumeInfo.timePoints.length;
|
||||
const label = dynamicVolumeInfo.splittingTag;
|
||||
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
|
||||
const dimensionGroupNumber = dynamicVolumeInfo.dimensionGroupNumber || 1;
|
||||
setDynamicInfo({
|
||||
volumeId: displaySet.displaySetInstanceUID,
|
||||
timePointIndex,
|
||||
numTimePoints,
|
||||
dimensionGroupNumber,
|
||||
numDimensionGroups,
|
||||
label,
|
||||
});
|
||||
} else {
|
||||
@@ -160,8 +160,7 @@ function RenderCinePlayer({
|
||||
dynamicInfo: dynamicInfoProp,
|
||||
customizationService,
|
||||
}) {
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
const CinePlayerComponent = customizationService.getCustomization('cinePlayer');
|
||||
|
||||
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
|
||||
|
||||
@@ -177,20 +176,20 @@ function RenderCinePlayer({
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTimePointIndexChange = evt => {
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
const handleDimensionGroupChange = evt => {
|
||||
const { volumeId, dimensionGroupNumber, numDimensionGroups, splittingTag } = evt.detail;
|
||||
setDynamicInfo({ volumeId, dimensionGroupNumber, numDimensionGroups, label: splittingTag });
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
Enums.Events.DYNAMIC_VOLUME_DIMENSION_GROUP_CHANGED,
|
||||
handleDimensionGroupChange
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
Enums.Events.DYNAMIC_VOLUME_DIMENSION_GROUP_CHANGED,
|
||||
handleDimensionGroupChange
|
||||
);
|
||||
};
|
||||
}, [dynamicInfo]);
|
||||
@@ -200,17 +199,17 @@ function RenderCinePlayer({
|
||||
return;
|
||||
}
|
||||
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||
const { volumeId, dimensionGroupNumber, numDimensionGroups, splittingTag } = dynamicInfo || {};
|
||||
const volume = cache.getVolume(volumeId, true);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
volume.dimensionGroupNumber = dimensionGroupNumber;
|
||||
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
setDynamicInfo({ volumeId, dimensionGroupNumber, numDimensionGroups, label: splittingTag });
|
||||
}, []);
|
||||
|
||||
const updateDynamicInfo = useCallback(props => {
|
||||
const { volumeId, timePointIndex } = props;
|
||||
const { volumeId, dimensionGroupNumber } = props;
|
||||
const volume = cache.getVolume(volumeId, true);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
volume.dimensionGroupNumber = dimensionGroupNumber;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -245,4 +244,4 @@ function RenderCinePlayer({
|
||||
);
|
||||
}
|
||||
|
||||
export default WrappedCinePlayer;
|
||||
export default WrappedCinePlayer;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon, ProgressLoadingBar } from '@ohif/ui';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Button } from '@ohif/ui';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
import DicomFileUploader, {
|
||||
EVENTS,
|
||||
UploadStatus,
|
||||
@@ -39,6 +41,11 @@ function DicomUploadProgress({
|
||||
dicomFileUploaderArr,
|
||||
onComplete,
|
||||
}: DicomUploadProgressProps): ReactElement {
|
||||
const { servicesManager } = useSystem();
|
||||
|
||||
const ProgressLoadingBar =
|
||||
servicesManager.services.customizationService.getCustomization('ui.progressLoadingBar');
|
||||
|
||||
const [totalUploadSize] = useState(
|
||||
dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0)
|
||||
);
|
||||
@@ -252,22 +259,24 @@ function DicomUploadProgress({
|
||||
}, [getPercentCompleteRounded, percentComplete]);
|
||||
|
||||
/**
|
||||
* Gets the css style for the 'n of m' (files completed) text. The only css attribute
|
||||
* of the style is width such that the 'n of m' is always a fixed width and thus
|
||||
* as each file completes uploading the text on screen does not constantly shift
|
||||
* left and right.
|
||||
* Gets the CSS style for the 'n of m' (files completed) text.
|
||||
* The width changes according to numFilesCompleted and can vary,
|
||||
* e.g. "1 of 200", "10 of 200", "100 of 200" all have differents width.
|
||||
*/
|
||||
const getNofMFilesStyle = useCallback(() => {
|
||||
// the number of digits accounts for the digits being on each side of the ' of '
|
||||
const numDigits = 2 * dicomFileUploaderArr.length.toString().length;
|
||||
// the number of digits + 2 spaces and 2 characters for ' of '
|
||||
const numChars = numDigits + 4;
|
||||
const numDigits =
|
||||
numFilesCompleted.toString().length + dicomFileUploaderArr.length.toString().length;
|
||||
// The number of digits + 3 additional characters (accounts for ' of ').
|
||||
// Even though intuitively 4 should be better, this is the most accurate width.
|
||||
// The font may play a part in this discrepancy.
|
||||
const numChars = numDigits + 3;
|
||||
return { width: `${numChars}ch` };
|
||||
}, []);
|
||||
}, [numFilesCompleted]);
|
||||
|
||||
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg">
|
||||
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg text-white">
|
||||
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||
<>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${dicomFileUploaderArr.length} ${
|
||||
@@ -283,21 +292,24 @@ function DicomUploadProgress({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap">
|
||||
<span
|
||||
style={getNofMFilesStyle()}
|
||||
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-right')}
|
||||
>
|
||||
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'}</span>
|
||||
<br />
|
||||
<span>
|
||||
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
style={getNofMFilesStyle()}
|
||||
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-end')}
|
||||
>
|
||||
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'} </span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
NO_WRAP_ELLIPSIS_CLASS_NAMES,
|
||||
'text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
|
||||
)}
|
||||
className={
|
||||
'text-primary hover:text-primary-lightactive:text-aqua-pale ml-auto cursor-pointer whitespace-nowrap'
|
||||
}
|
||||
onClick={cancelAllUploads}
|
||||
>
|
||||
Cancel All Uploads
|
||||
@@ -313,10 +325,10 @@ function DicomUploadProgress({
|
||||
<div className="ml-auto flex w-6 justify-center">
|
||||
{numFails > 0 && (
|
||||
<div onClick={() => setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)}>
|
||||
<Icon
|
||||
<Icons.ByName
|
||||
className="cursor-pointer"
|
||||
name="icon-status-alert"
|
||||
></Icon>
|
||||
></Icons.ByName>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -347,7 +359,7 @@ function DicomUploadProgress({
|
||||
></ProgressLoadingBar>
|
||||
</div>
|
||||
<div className="ml-1 flex w-24 items-center">
|
||||
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
|
||||
<div className="w-10 text-right text-foreground">{`${getPercentCompleteRounded()}%`}</div>
|
||||
{getShowFailedOnlyIconComponent()}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,7 @@ import DicomFileUploader, {
|
||||
UploadRejection,
|
||||
UploadStatus,
|
||||
} from '../../utils/DicomFileUploader';
|
||||
import { Icon } from '@ohif/ui';
|
||||
import { Icons } from '@ohif/ui-next';
|
||||
|
||||
type DicomUploadProgressItemProps = {
|
||||
dicomFileUploader: DicomFileUploader;
|
||||
@@ -54,17 +54,17 @@ const DicomUploadProgressItem = memo(
|
||||
switch (dicomFileUploader.getStatus()) {
|
||||
case UploadStatus.Success:
|
||||
return (
|
||||
<Icon
|
||||
<Icons.ByName
|
||||
name="status-tracked"
|
||||
className="text-primary-light"
|
||||
></Icon>
|
||||
/>
|
||||
);
|
||||
case UploadStatus.InProgress:
|
||||
return <Icon name="icon-transferring"></Icon>;
|
||||
return <Icons.ByName name="icon-transferring" />;
|
||||
case UploadStatus.Failed:
|
||||
return <Icon name="icon-alert-small"></Icon>;
|
||||
return <Icons.ByName name="icon-alert-small" />;
|
||||
case UploadStatus.Cancelled:
|
||||
return <Icon name="icon-alert-outline"></Icon>;
|
||||
return <Icons.ByName name="icon-alert-outline" />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ const DicomUploadProgressItem = memo(
|
||||
<div className="self-top flex w-0 shrink grow flex-col gap-1">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex w-6 shrink-0 items-center justify-center">{getStatusIcon()}</div>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-white">
|
||||
{dicomFileUploader.getFileName()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,9 +88,8 @@ const DicomUploadProgressItem = memo(
|
||||
<div className="w-10 text-right">{percentComplete}%</div>
|
||||
)}
|
||||
<div className="ml-auto flex cursor-pointer">
|
||||
<Icon
|
||||
className="text-primary-active self-center"
|
||||
name="close"
|
||||
<Icons.Close
|
||||
className="text-primary self-center"
|
||||
onClick={cancelUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Accordion, AccordionContent, AccordionItem } from '@ohif/ui-next';
|
||||
|
||||
import PanelAccordionTrigger from './PanelAccordionTrigger';
|
||||
import MeasurementsMenu from './MeasurementsMenu';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
export function MeasurementItem(props) {
|
||||
const { index, item } = props;
|
||||
return (
|
||||
<PanelAccordionTrigger
|
||||
count={index + 1}
|
||||
text={item.toolName || item.label || item.title}
|
||||
colorHex="#f00"
|
||||
isActive={item.isSelected}
|
||||
menu={MeasurementsMenu}
|
||||
group={{ items: [item], onClick: props.onClick }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MeasurementAccordion(props) {
|
||||
const { items } = props;
|
||||
const system = useSystem();
|
||||
|
||||
const onClick = (e, group) => {
|
||||
const { items } = group;
|
||||
// Just jump to the first measurement in the set, and mark that one as active
|
||||
// with the set of items.
|
||||
system.commandsManager.run('jumpToMeasurement', {
|
||||
uid: items[0].uid,
|
||||
displayMeasurements: items,
|
||||
group,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="flex-shrink-0 overflow-hidden"
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
const { displayText: details = {} } = item;
|
||||
return (
|
||||
<AccordionItem
|
||||
key={`measurementAccordion:${item.uid}`}
|
||||
value={item.uid}
|
||||
>
|
||||
<MeasurementItem
|
||||
item={item}
|
||||
key={`measurementItem:${item.uid}`}
|
||||
index={index}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<AccordionContent key={`measurementContent:${item.uid}`}>
|
||||
<div className="ml-7 px-2 py-2">
|
||||
<div className="text-secondary-foreground flex items-center gap-1 text-base leading-normal">
|
||||
{details.primary?.length > 0 &&
|
||||
details.primary.map((detail, index) => (
|
||||
<span key={`details:${item.uid}:${index}`}>{detail}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { MeasurementTable } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* This is a measurement table that is designed to be nested inside
|
||||
* the accordion groups.
|
||||
*/
|
||||
export default function MeasurementTableNested(props) {
|
||||
const { title, items, group, customHeader } = props;
|
||||
const { commandsManager } = useSystem();
|
||||
const onAction = (e, command, uid) => {
|
||||
commandsManager.run(command, { uid, annotationUID: uid, displayMeasurements: items });
|
||||
};
|
||||
|
||||
return (
|
||||
<MeasurementTable
|
||||
title={title ? title : `Measurements`}
|
||||
data={items}
|
||||
onAction={onAction}
|
||||
{...group}
|
||||
key={group.key}
|
||||
>
|
||||
<MeasurementTable.Header key="measurementTableHeader">
|
||||
{customHeader && group.isFirst && customHeader({ ...props, items: props.allItems })}
|
||||
</MeasurementTable.Header>
|
||||
<MeasurementTable.Body key="measurementTableBody" />
|
||||
</MeasurementTable>
|
||||
);
|
||||
}
|
||||
Loaded 100 of 2822 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user