volview 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.browserslistrc +4 -0
  2. package/.editorconfig +6 -0
  3. package/.env +6 -0
  4. package/.eslintrc.js +109 -0
  5. package/.gitattributes +1 -0
  6. package/.github/workflows/checks.yml +16 -0
  7. package/.github/workflows/publish.yml +52 -0
  8. package/.prettierignore +5 -0
  9. package/CONTRIBUTING.md +29 -0
  10. package/LICENSE +202 -0
  11. package/README.md +44 -0
  12. package/babel.config.js +3 -0
  13. package/documentation/config.js +15 -0
  14. package/documentation/content/KWVolViewLogo.png +0 -0
  15. package/documentation/content/KWVolViewLogo.svg +35 -0
  16. package/documentation/content/VolViewLogo.png +0 -0
  17. package/documentation/content/VolViewLogo.svg +22 -0
  18. package/documentation/content/doc/index.md +64 -0
  19. package/documentation/content/doc/loading_data.md +30 -0
  20. package/documentation/content/doc/mouse_controls.md +34 -0
  21. package/documentation/content/doc/quick_start_guide.md +75 -0
  22. package/documentation/content/doc/rendering.md +27 -0
  23. package/documentation/content/doc/state_files.md +18 -0
  24. package/documentation/content/doc/toolbar.md +20 -0
  25. package/documentation/content/doc/welcome_screen.md +23 -0
  26. package/documentation/content/gallery/01-volview-welcome-notes.jpg +0 -0
  27. package/documentation/content/gallery/01-volview-welcome.jpg +0 -0
  28. package/documentation/content/gallery/02-volview-about.jpg +0 -0
  29. package/documentation/content/gallery/02-volview-notifications.jpg +0 -0
  30. package/documentation/content/gallery/02-volview-settings.jpg +0 -0
  31. package/documentation/content/gallery/03-volview-sample-datasets.jpg +0 -0
  32. package/documentation/content/gallery/04-volview-active-data.jpg +0 -0
  33. package/documentation/content/gallery/05-volview-select-data-to-delete.jpg +0 -0
  34. package/documentation/content/gallery/06-volview-corner-annotation.jpg +0 -0
  35. package/documentation/content/gallery/07-volview-layout-notes.jpg +0 -0
  36. package/documentation/content/gallery/07-volview-layout.jpg +0 -0
  37. package/documentation/content/gallery/09-volview-layout-3DPrimary.jpg +0 -0
  38. package/documentation/content/gallery/10-volview-wl-pan-zoom-notes.jpg +0 -0
  39. package/documentation/content/gallery/10-volview-wl-pan-zoom.jpg +0 -0
  40. package/documentation/content/gallery/11-volview-paint-notes.jpg +0 -0
  41. package/documentation/content/gallery/11-volview-paint.jpg +0 -0
  42. package/documentation/content/gallery/12-volview-paint-options.jpg +0 -0
  43. package/documentation/content/gallery/13-volview-crop-notes.jpg +0 -0
  44. package/documentation/content/gallery/13-volview-crop.jpg +0 -0
  45. package/documentation/content/gallery/14-volview-ruler.jpg +0 -0
  46. package/documentation/content/gallery/15-volview-crosshairs.jpg +0 -0
  47. package/documentation/content/gallery/16-volview-rendering.jpg +0 -0
  48. package/documentation/content/gallery/17-volview-colormap-notes.jpg +0 -0
  49. package/documentation/content/gallery/17-volview-colormap.jpg +0 -0
  50. package/documentation/content/gallery/17-volview-opacity-notes.jpg +0 -0
  51. package/documentation/content/gallery/17-volview-opacity.jpg +0 -0
  52. package/documentation/content/gallery/17-volview-opacity2.jpg +0 -0
  53. package/documentation/content/gallery/18-volview-presets.jpg +0 -0
  54. package/documentation/content/gallery/19-volview-color.jpg +0 -0
  55. package/documentation/content/gallery/19-volview-color2.jpg +0 -0
  56. package/documentation/content/gallery/20-volview-lightfollowcamera0.jpg +0 -0
  57. package/documentation/content/gallery/20-volview-lightfollowcamera1.jpg +0 -0
  58. package/documentation/content/gallery/20-volview-lightfollowcamera2.jpg +0 -0
  59. package/documentation/content/gallery/20-volview-lightfollowcamera3.jpg +0 -0
  60. package/documentation/content/gallery/21-volview-hybrid-final.jpg +0 -0
  61. package/documentation/content/gallery/21-volview-hybrid0.0.jpg +0 -0
  62. package/documentation/content/gallery/21-volview-hybrid0.5.jpg +0 -0
  63. package/documentation/content/gallery/21-volview-hybrid1.0.jpg +0 -0
  64. package/documentation/content/gallery/LocalAmbientOcclusion.jpg +0 -0
  65. package/documentation/content/gallery/Notes.pptx +0 -0
  66. package/documentation/content/gallery/VolView-Overview.jpg +0 -0
  67. package/documentation/content/gallery/index.md +53 -0
  68. package/documentation/content/icon/favicon-16x16.png +0 -0
  69. package/documentation/content/icon/favicon-196x196.png +0 -0
  70. package/documentation/content/icon/favicon-32x32.png +0 -0
  71. package/documentation/content/icon/favicon-96x96.png +0 -0
  72. package/documentation/content/index.pug +43 -0
  73. package/documentation/content/logo.png +0 -0
  74. package/documentation/content/logo.svg +20 -0
  75. package/documentation/data/menu.yml +3 -0
  76. package/documentation/themes/navy/layout/partial/footer.swig +22 -0
  77. package/documentation/tpl/__en__ +31 -0
  78. package/documentation/tpl/__sidebar__ +11 -0
  79. package/package.json +96 -0
  80. package/patches/jsdom-global+3.0.2.patch +12 -0
  81. package/patches/vue-toastification+1.7.14.patch +41 -0
  82. package/patches/vuetify+2.6.10.patch +13 -0
  83. package/prettier.config.js +9 -0
  84. package/public/favicon.ico +0 -0
  85. package/public/global.css +20 -0
  86. package/public/index.html +28 -0
  87. package/public/pvm-sample.png +0 -0
  88. package/server/helper.py +147 -0
  89. package/server/host.js +134 -0
  90. package/server/serialize.js +111 -0
  91. package/server/serialize.py +203 -0
  92. package/server/server.py +84 -0
  93. package/server/test.py +17 -0
  94. package/src/assets/KitwareHeadAndNeck.jpg +0 -0
  95. package/src/assets/logo.png +0 -0
  96. package/src/assets/logo.svg +20 -0
  97. package/src/assets/samples/3DUS-Fetus.jpg +0 -0
  98. package/src/assets/samples/CTA-Head_and_Neck.jpg +0 -0
  99. package/src/assets/samples/MRA-Head_and_Neck.jpg +0 -0
  100. package/src/assets/samples/MRI-Cardiac.jpg +0 -0
  101. package/src/assets/samples/MRI-PROSTATEx.jpg +0 -0
  102. package/src/components/AboutBox.vue +157 -0
  103. package/src/components/AiModule.vue +60 -0
  104. package/src/components/AnnotationsModule.vue +37 -0
  105. package/src/components/App.vue +539 -0
  106. package/src/components/ColorFunctionSlider.vue +181 -0
  107. package/src/components/DataBrowser.vue +173 -0
  108. package/src/components/DragAndDrop.vue +107 -0
  109. package/src/components/GroupableItem.vue +30 -0
  110. package/src/components/ImageDataBrowser.vue +186 -0
  111. package/src/components/ImageListCard.vue +114 -0
  112. package/src/components/ItemGroup.vue +74 -0
  113. package/src/components/LabelmapList.vue +36 -0
  114. package/src/components/LayoutGrid.vue +96 -0
  115. package/src/components/MeasurementsList.vue +134 -0
  116. package/src/components/MessageCenter.vue +189 -0
  117. package/src/components/MessageItem.vue +83 -0
  118. package/src/components/MessageNotificationContent.vue +20 -0
  119. package/src/components/MessageNotifications.vue +84 -0
  120. package/src/components/ModelBrowser.vue +83 -0
  121. package/src/components/ModulePanel.vue +137 -0
  122. package/src/components/PaintControls.vue +174 -0
  123. package/src/components/PatientBrowser.vue +280 -0
  124. package/src/components/PatientStudyVolumeBrowser.vue +325 -0
  125. package/src/components/RenderingModule.vue +49 -0
  126. package/src/components/ResizableNavDrawer.vue +158 -0
  127. package/src/components/SampleDataBrowser.vue +198 -0
  128. package/src/components/SaveSession.vue +57 -0
  129. package/src/components/Settings.vue +35 -0
  130. package/src/components/SliceSlider.vue +182 -0
  131. package/src/components/SpleenModule.vue +101 -0
  132. package/src/components/ToolButton.vue +58 -0
  133. package/src/components/ToolStrip.vue +186 -0
  134. package/src/components/ViewOverlayGrid.vue +84 -0
  135. package/src/components/VolumePresets.vue +165 -0
  136. package/src/components/VolumeProperties.vue +171 -0
  137. package/src/components/VolumeRendering.vue +365 -0
  138. package/src/components/VtkThreeView.vue +512 -0
  139. package/src/components/VtkTwoView.vue +665 -0
  140. package/src/components/__tests__/ResizableNavDrawer.spec.js +32 -0
  141. package/src/components/__tests__/ToolButton.spec.js +46 -0
  142. package/src/components/icons/KitwareLogoIcon.vue +54 -0
  143. package/src/components/icons/VolViewFullLogo.vue +139 -0
  144. package/src/components/icons/VolViewLogo.vue +78 -0
  145. package/src/components/styles/utils.css +16 -0
  146. package/src/components/styles/vtk-view.css +116 -0
  147. package/src/components/tools/CropTool.vue +61 -0
  148. package/src/components/tools/CrosshairsTool.vue +93 -0
  149. package/src/components/tools/MouseManipulatorTool.vue +108 -0
  150. package/src/components/tools/PaintTool.vue +54 -0
  151. package/src/components/tools/PanTool.vue +35 -0
  152. package/src/components/tools/RulerTool.vue +202 -0
  153. package/src/components/tools/SliceScrollTool.vue +123 -0
  154. package/src/components/tools/WindowLevelTool.vue +191 -0
  155. package/src/components/tools/ZoomTool.vue +35 -0
  156. package/src/components/tools/crop/Crop2D.vue +367 -0
  157. package/src/components/tools/crop/Crop2DLineHandle.vue +103 -0
  158. package/src/components/tools/crop/Crop3D.vue +192 -0
  159. package/src/components/tools/crop/CropControls.vue +38 -0
  160. package/src/components/tools/crop/types.ts +12 -0
  161. package/src/components/tools/crosshairs/CrosshairSVG2D.vue +125 -0
  162. package/src/components/tools/crosshairs/CrosshairsWidget2D.vue +100 -0
  163. package/src/components/tools/paint/PaintWidget2D.vue +192 -0
  164. package/src/components/tools/ruler/RulerSVG2D.vue +181 -0
  165. package/src/components/tools/ruler/RulerWidget2D.vue +154 -0
  166. package/src/composables/__tests__/useDoubleRecord.spec.ts +57 -0
  167. package/src/composables/__tests__/useOrientationLabels.spec.ts +29 -0
  168. package/src/composables/isViewAnimating.ts +25 -0
  169. package/src/composables/manageVTKSubscription.ts +6 -0
  170. package/src/composables/onProxyManagerEvent.ts +50 -0
  171. package/src/composables/proxyManager.ts +13 -0
  172. package/src/composables/sync/__tests__/createPrimitiveSyncContext.spec.ts +41 -0
  173. package/src/composables/sync/__tests__/useWindowingSync.spec.ts +51 -0
  174. package/src/composables/sync/createPrimitiveSyncContext.ts +83 -0
  175. package/src/composables/sync/useWindowingSync.ts +48 -0
  176. package/src/composables/useAppLoadingNotifications.ts +73 -0
  177. package/src/composables/useCameraOrientation.ts +34 -0
  178. package/src/composables/useCurrentImage.ts +74 -0
  179. package/src/composables/useCurrentSlice.ts +43 -0
  180. package/src/composables/useDoubleRecord.ts +77 -0
  181. package/src/composables/useEventListener.ts +26 -0
  182. package/src/composables/useGlobalErrorHook.ts +18 -0
  183. package/src/composables/useMultiSelection.ts +29 -0
  184. package/src/composables/useOrientationLabels.ts +62 -0
  185. package/src/composables/usePanels.ts +42 -0
  186. package/src/composables/usePersistCameraConfig.ts +138 -0
  187. package/src/composables/useResizeObserver.ts +37 -0
  188. package/src/composables/useResizeToFit.ts +50 -0
  189. package/src/composables/useSceneBuilder.ts +96 -0
  190. package/src/composables/useToast.js +11 -0
  191. package/src/composables/useVTKCallback.ts +33 -0
  192. package/src/composables/useVTKWorldToDisplay.ts +141 -0
  193. package/src/composables/useViewProxy.ts +87 -0
  194. package/src/composables/useVolumeThumbnailing.ts +173 -0
  195. package/src/composables/useWebGLWatchdog.ts +32 -0
  196. package/src/composables/useWidgetManager.ts +41 -0
  197. package/src/config.ts +182 -0
  198. package/src/constants.ts +38 -0
  199. package/src/core/id.ts +18 -0
  200. package/src/core/provider.ts +31 -0
  201. package/src/core/proxies.ts +90 -0
  202. package/src/core/thumbnailers/__tests__/vtk-image.spec.ts +58 -0
  203. package/src/core/thumbnailers/index.ts +5 -0
  204. package/src/core/thumbnailers/volume-thumbnailer.ts +130 -0
  205. package/src/core/thumbnailers/vtk-image.ts +142 -0
  206. package/src/core/tools/__tests__/paint.spec.ts +101 -0
  207. package/src/core/tools/__tests__/ruler.spec.ts +73 -0
  208. package/src/core/tools/paint/brush.ts +20 -0
  209. package/src/core/tools/paint/ellipse-brush.ts +96 -0
  210. package/src/core/tools/paint/index.ts +141 -0
  211. package/src/core/tools/ruler.ts +149 -0
  212. package/src/global.d.ts +24 -0
  213. package/src/io/__tests__/io.spec.ts +26 -0
  214. package/src/io/dicom.ts +187 -0
  215. package/src/io/index.ts +11 -0
  216. package/src/io/io.ts +157 -0
  217. package/src/io/itk/itkConfig.js +8 -0
  218. package/src/io/itk-dicom/dicom.cpp +327 -0
  219. package/src/io/itk-dicom/web-build/dicom.js +16 -0
  220. package/src/io/itk-dicom/web-build/dicom.umd.js +21 -0
  221. package/src/io/itk-dicom/web-build/dicom.umd.wasm +0 -0
  222. package/src/io/itk-dicom/web-build/dicom.wasm +0 -0
  223. package/src/io/manifest.ts +30 -0
  224. package/src/io/readers.ts +36 -0
  225. package/src/io/state-file/index.ts +170 -0
  226. package/src/io/state-file/schema.ts +277 -0
  227. package/src/io/state-file/utils.ts +33 -0
  228. package/src/io/types.ts +4 -0
  229. package/src/io/vtk/async.reader.worker.ts +50 -0
  230. package/src/io/vtk/async.ts +58 -0
  231. package/src/io/vtk/async.writer.worker.ts +46 -0
  232. package/src/io/vtk/common.ts +30 -0
  233. package/src/io/zip.ts +31 -0
  234. package/src/main.js +66 -0
  235. package/src/main.ts +1 -0
  236. package/src/plugins/vuetify.js +25 -0
  237. package/src/shims-itk.d.ts +67 -0
  238. package/src/shims-misc.d.ts +4 -0
  239. package/src/shims-tsx.d.ts +13 -0
  240. package/src/shims-vtk.d.ts +1042 -0
  241. package/src/shims-vue.d.ts +5 -0
  242. package/src/store/__tests__/datasets.spec.ts +75 -0
  243. package/src/store/__tests__/messages.spec.ts +74 -0
  244. package/src/store/__tests__/rulers.spec.ts +77 -0
  245. package/src/store/__tests__/view-configs.spec.ts +37 -0
  246. package/src/store/datasets-dicom.ts +398 -0
  247. package/src/store/datasets-files.ts +35 -0
  248. package/src/store/datasets-images.ts +118 -0
  249. package/src/store/datasets-labelmaps.ts +136 -0
  250. package/src/store/datasets-models.ts +25 -0
  251. package/src/store/datasets.ts +341 -0
  252. package/src/store/messages.ts +144 -0
  253. package/src/store/tools/crop.ts +136 -0
  254. package/src/store/tools/crosshairs.ts +131 -0
  255. package/src/store/tools/index.ts +95 -0
  256. package/src/store/tools/paint.ts +191 -0
  257. package/src/store/tools/rulers.ts +305 -0
  258. package/src/store/tools/types.ts +54 -0
  259. package/src/store/view-configs/camera.ts +51 -0
  260. package/src/store/view-configs/common.ts +61 -0
  261. package/src/store/view-configs/slicing.ts +86 -0
  262. package/src/store/view-configs/types.ts +39 -0
  263. package/src/store/view-configs/volume-coloring.ts +193 -0
  264. package/src/store/view-configs/windowing.ts +69 -0
  265. package/src/store/view-configs.ts +78 -0
  266. package/src/store/views.ts +130 -0
  267. package/src/types/index.ts +16 -0
  268. package/src/types/layout.ts +10 -0
  269. package/src/types/lps.ts +40 -0
  270. package/src/types/views.ts +65 -0
  271. package/src/types/vtk-types.ts +29 -0
  272. package/src/utils/__tests__/lps.spec.ts +102 -0
  273. package/src/utils/color.ts +23 -0
  274. package/src/utils/errors.js +3 -0
  275. package/src/utils/fetch.ts +101 -0
  276. package/src/utils/hacks.ts +14 -0
  277. package/src/utils/index.ts +83 -0
  278. package/src/utils/lps.ts +129 -0
  279. package/src/utils/manipulators.ts +35 -0
  280. package/src/utils/promise-worker.ts +45 -0
  281. package/src/utils/vtk-helpers.ts +215 -0
  282. package/src/utils/workerHandler.js +42 -0
  283. package/src/vtk/ColorMaps.ts +72 -0
  284. package/src/vtk/CrosshairsWidget/behavior.ts +112 -0
  285. package/src/vtk/CrosshairsWidget/index.d.ts +22 -0
  286. package/src/vtk/CrosshairsWidget/index.js +73 -0
  287. package/src/vtk/CrosshairsWidget/state.ts +51 -0
  288. package/src/vtk/IJKSliceRepresentationProxy/index.d.ts +9 -0
  289. package/src/vtk/IJKSliceRepresentationProxy/index.js +59 -0
  290. package/src/vtk/LPSView2DProxy/index.d.ts +18 -0
  291. package/src/vtk/LPSView2DProxy/index.js +89 -0
  292. package/src/vtk/LPSView3DProxy/index.d.ts +16 -0
  293. package/src/vtk/LPSView3DProxy/index.js +78 -0
  294. package/src/vtk/LabelMap/index.d.ts +16 -0
  295. package/src/vtk/LabelMap/index.js +49 -0
  296. package/src/vtk/LabelMapSliceRepProxy/index.d.ts +6 -0
  297. package/src/vtk/LabelMapSliceRepProxy/index.js +104 -0
  298. package/src/vtk/MedicalColorPresets.json +1448 -0
  299. package/src/vtk/PaintBrushContextRepresentation/index.js +191 -0
  300. package/src/vtk/PaintWidget/behavior.ts +101 -0
  301. package/src/vtk/PaintWidget/index.d.ts +28 -0
  302. package/src/vtk/PaintWidget/index.js +56 -0
  303. package/src/vtk/PaintWidget/state.ts +41 -0
  304. package/src/vtk/PiecewiseWidget/index.js +203 -0
  305. package/src/vtk/RulerWidget/behavior.ts +185 -0
  306. package/src/vtk/RulerWidget/index.d.ts +25 -0
  307. package/src/vtk/RulerWidget/index.js +79 -0
  308. package/src/vtk/RulerWidget/state.ts +63 -0
  309. package/src/vtk/TreJsonConverter/index.js +150 -0
  310. package/src/vtk/proxy.js +66 -0
  311. package/src/vtk/proxyUtils.js +16 -0
  312. package/src/vtk/webvr-empty.js +1 -0
  313. package/tests/testUtils.js +12 -0
  314. package/tsconfig.json +46 -0
  315. package/vue.config.js +86 -0
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import Vue from 'vue';
3
+
4
+ export default Vue;
5
+ }
@@ -0,0 +1,75 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiSubset from 'chai-subset';
3
+
4
+ import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
5
+
6
+ import { setActivePinia, createPinia } from 'pinia';
7
+ import { useDatasetStore } from '@src/store/datasets';
8
+ import { makeEmptyFile } from '@/tests/testUtils';
9
+ import { FILE_READERS } from '@/src/io';
10
+ import { CorePiniaProviderPlugin } from '@/src/core/provider';
11
+ import ProxyWrapper from '@/src/core/proxies';
12
+ import Sinon from 'sinon';
13
+
14
+ chai.use(chaiSubset);
15
+
16
+ describe('Dataset store', () => {
17
+ let proxies = Sinon.createStubInstance(ProxyWrapper);
18
+
19
+ beforeEach(() => {
20
+ proxies = Sinon.createStubInstance(ProxyWrapper);
21
+
22
+ const pinia = createPinia();
23
+ pinia.use(
24
+ CorePiniaProviderPlugin({
25
+ proxies,
26
+ })
27
+ );
28
+
29
+ setActivePinia(pinia);
30
+ });
31
+
32
+ it('loads images', async () => {
33
+ const datasetStore = useDatasetStore();
34
+ const files = [
35
+ makeEmptyFile('test1.nrrd'),
36
+ makeEmptyFile('test2.nrrd'),
37
+ makeEmptyFile('test3.nrrd'),
38
+ ];
39
+
40
+ // override nrrd reader
41
+ const testImageData = vtkImageData.newInstance();
42
+ FILE_READERS.set('nrrd', () => testImageData);
43
+
44
+ const loadResults = await datasetStore.loadFiles(files);
45
+ expect(loadResults).to.containSubset([
46
+ { type: 'file', filename: 'test1.nrrd', loaded: true, dataType: 'image' },
47
+ { type: 'file', filename: 'test2.nrrd', loaded: true, dataType: 'image' },
48
+ { type: 'file', filename: 'test3.nrrd', loaded: true, dataType: 'image' },
49
+ ]);
50
+ });
51
+
52
+ it('handles missing readers', async () => {
53
+ const datasetStore = useDatasetStore();
54
+ const files = [makeEmptyFile('test1.invalid')];
55
+
56
+ const loadResults = await datasetStore.loadFiles(files);
57
+ expect(loadResults).to.containSubset([
58
+ { type: 'file', filename: 'test1.invalid', loaded: false },
59
+ ]);
60
+ });
61
+
62
+ it('handles readers that return an error', async () => {
63
+ const datasetStore = useDatasetStore();
64
+ const files = [makeEmptyFile('test1.invalid')];
65
+
66
+ FILE_READERS.set('invalid', () => {
67
+ throw new Error('invalid!');
68
+ });
69
+
70
+ const loadResults = await datasetStore.loadFiles(files);
71
+ expect(loadResults).to.containSubset([
72
+ { type: 'file', filename: 'test1.invalid', loaded: false },
73
+ ]);
74
+ });
75
+ });
@@ -0,0 +1,74 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiAsPromised from 'chai-as-promised';
3
+
4
+ import { setActivePinia, createPinia } from 'pinia';
5
+
6
+ import { MessageType, useMessageStore } from '@src/store/messages';
7
+
8
+ chai.use(chaiAsPromised);
9
+
10
+ describe('Message store', () => {
11
+ beforeEach(() => {
12
+ setActivePinia(createPinia());
13
+ });
14
+
15
+ it('supports adding, accessing, and deleting messages', () => {
16
+ const messageStore = useMessageStore();
17
+
18
+ const innerError = new Error('inner error');
19
+ const ids = [
20
+ messageStore.addError('an error', innerError),
21
+ messageStore.addWarning('warning'),
22
+ messageStore.addInfo('info'),
23
+ messageStore.addInfo('loading', {
24
+ details: 'Loading files',
25
+ persist: true,
26
+ }),
27
+ ];
28
+
29
+ const expected = [
30
+ {
31
+ type: MessageType.Error,
32
+ title: 'an error',
33
+ options: {
34
+ details: String(innerError),
35
+ persist: false,
36
+ },
37
+ },
38
+ {
39
+ type: MessageType.Warning,
40
+ title: 'warning',
41
+ options: {
42
+ persist: false,
43
+ },
44
+ },
45
+ {
46
+ type: MessageType.Info,
47
+ title: 'info',
48
+ options: {
49
+ persist: false,
50
+ },
51
+ },
52
+ {
53
+ type: MessageType.Info,
54
+ title: 'loading',
55
+ options: {
56
+ details: 'Loading files',
57
+ persist: true,
58
+ },
59
+ },
60
+ ].map((ex, i) => ({ ...ex, id: String(i + 1) }));
61
+
62
+ expect(messageStore.messages).to.have.length(4);
63
+
64
+ ids.forEach((id, index) => {
65
+ expect(messageStore.byID[id]).to.deep.equal(expected[index]);
66
+ });
67
+
68
+ messageStore.clearOne(ids[1]);
69
+ expect(messageStore.byID).to.not.have.property(ids[1]);
70
+
71
+ messageStore.clearAll();
72
+ expect(messageStore.messages).to.be.empty;
73
+ });
74
+ });
@@ -0,0 +1,77 @@
1
+ import chai, { expect } from 'chai';
2
+ import chaiSubset from 'chai-subset';
3
+
4
+ import { setActivePinia, createPinia } from 'pinia';
5
+ import { CorePiniaProviderPlugin } from '@/src/core/provider';
6
+ import RulerTool from '@/src/core/tools/ruler';
7
+ import { useRulerStore } from '@/src/store/tools/rulers';
8
+ import { InteractionState } from '@/src/vtk/RulerWidget/state';
9
+ import { Vector3 } from '@kitware/vtk.js/types';
10
+ import { LPSAxis } from '@/src/types/lps';
11
+
12
+ chai.use(chaiSubset);
13
+
14
+ describe('Ruler store', () => {
15
+ let rulerCore: RulerTool;
16
+ let rulerStore: ReturnType<typeof useRulerStore>;
17
+
18
+ beforeEach(() => {
19
+ rulerCore = new RulerTool();
20
+
21
+ const pinia = createPinia();
22
+ pinia.use(
23
+ CorePiniaProviderPlugin({
24
+ rulers: rulerCore,
25
+ })
26
+ );
27
+
28
+ setActivePinia(pinia);
29
+
30
+ rulerStore = useRulerStore();
31
+ rulerStore.initialize();
32
+ });
33
+
34
+ afterEach(() => {
35
+ rulerStore.uninitialize();
36
+ });
37
+
38
+ it('should support CRUD operations', () => {
39
+ const id = rulerStore.addNewRuler({});
40
+ expect(rulerStore.rulerIDs).to.include(id);
41
+
42
+ const patch = {
43
+ name: 'ruler #1',
44
+ firstPoint: [0, 0, 1] as Vector3,
45
+ secondPoint: [0, 0, 2] as Vector3,
46
+ viewAxis: 'Axial' as LPSAxis,
47
+ slice: 50,
48
+ imageID: '3',
49
+ interactionState: InteractionState.Settled,
50
+ color: '#ccc',
51
+ };
52
+ rulerStore.updateRuler(id, patch);
53
+ expect(rulerStore.rulers[id]).to.deep.equal(patch);
54
+
55
+ rulerStore.removeRuler(id);
56
+ expect(rulerStore.rulerIDs).to.not.include(id);
57
+ });
58
+
59
+ it('should sync state with the injected RulerTool', () => {
60
+ const id = rulerStore.addNewRuler({});
61
+ const factory = rulerCore.getFactory(id);
62
+ expect(factory).to.not.be.null;
63
+
64
+ const state = factory!.getWidgetState();
65
+ state.getFirstPoint().setOrigin([1, 2, 3]);
66
+ expect(rulerStore.rulers[id].firstPoint).to.deep.equal([1, 2, 3]);
67
+
68
+ rulerStore.updateRuler(id, {
69
+ secondPoint: [4, 5, 6],
70
+ });
71
+
72
+ expect(state.getSecondPoint().getOrigin()).to.deep.equal([4, 5, 6]);
73
+ });
74
+
75
+ // addRulerFromViewEvent requires an integration test due to
76
+ // its dependency on vtkOpenGLRenderWindow
77
+ });
@@ -0,0 +1,37 @@
1
+ import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy';
2
+ import chai, { expect } from 'chai';
3
+ import chaiAsPromised from 'chai-as-promised';
4
+
5
+ import { setActivePinia, createPinia } from 'pinia';
6
+ import { useViewConfigStore } from '../view-configs';
7
+
8
+ chai.use(chaiAsPromised);
9
+
10
+ describe('View Config Store', () => {
11
+ beforeEach(() => {
12
+ setActivePinia(createPinia());
13
+ });
14
+
15
+ it('supports setting and getting volume coloring configs', () => {
16
+ const configStore = useViewConfigStore();
17
+
18
+ configStore.updateVolumeColorConfig('1', '2', {
19
+ colorBy: {
20
+ arrayName: 'Scalars',
21
+ location: 'pointData',
22
+ },
23
+ transferFunction: {
24
+ preset: 'Hot',
25
+ mappingRange: [0, 1],
26
+ },
27
+ opacityFunction: {
28
+ mode: vtkPiecewiseFunctionProxy.Mode.Gaussians,
29
+ gaussians: [],
30
+ mappingRange: [0, 1],
31
+ },
32
+ });
33
+
34
+ const config = configStore.getVolumeColorConfig('1', '2');
35
+ expect(config).to.not.be.undefined;
36
+ });
37
+ });
@@ -0,0 +1,398 @@
1
+ import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
2
+ import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
3
+ import { set, del } from '@vue/composition-api';
4
+ import { defineStore } from 'pinia';
5
+ import { pick, removeFromArray } from '../utils';
6
+ import { useImageStore } from './datasets-images';
7
+ import { useFileStore } from './datasets-files';
8
+ import { StateFile, DataSetType, DataSet } from '../io/state-file/schema';
9
+ import { serializeData } from '../io/state-file/utils';
10
+
11
+ export const ANONYMOUS_PATIENT = 'Anonymous';
12
+ export const ANONYMOUS_PATIENT_ID = 'ANONYMOUS';
13
+
14
+ export function imageCacheMultiKey(offset: number, asThumbnail: boolean) {
15
+ return `${offset}!!${asThumbnail}`;
16
+ }
17
+
18
+ export interface VolumeKeys {
19
+ patientKey: string;
20
+ studyKey: string;
21
+ volumeKey: string;
22
+ }
23
+
24
+ export interface PatientInfo {
25
+ PatientID: string;
26
+ PatientName: string;
27
+ PatientBirthDate: string;
28
+ PatientSex: string;
29
+ }
30
+
31
+ export interface StudyInfo {
32
+ StudyID: string;
33
+ StudyInstanceUID: string;
34
+ StudyDate: string;
35
+ StudyTime: string;
36
+ AccessionNumber: string;
37
+ StudyDescription: string;
38
+ }
39
+
40
+ export interface VolumeInfo {
41
+ NumberOfSlices: number;
42
+ VolumeID: string;
43
+ Modality: string;
44
+ SeriesInstanceUID: string;
45
+ SeriesNumber: string;
46
+ SeriesDescription: string;
47
+ }
48
+
49
+ /**
50
+ * Generate a synthetic multi-key patient key from a Patient object.
51
+ *
52
+ * This key is used to try to uniquely identify a patient, since the PatientID
53
+ * is not guaranteed to be unique (especially in the anonymous case).
54
+ *
55
+ * Required keys in the Patient object:
56
+ * - PatientName
57
+ * - PatientID
58
+ * - PatientBirthDate
59
+ * - PatientSex
60
+ *
61
+ * @param {Patient} patient
62
+ */
63
+ export function genSynPatientKey(patient: PatientInfo) {
64
+ const pid = patient.PatientID.trim();
65
+ const name = patient.PatientName.trim();
66
+ const bdate = patient.PatientBirthDate.trim();
67
+ const sex = patient.PatientSex.trim();
68
+ // we only care about making a unique key here. The
69
+ // data doesn't actually matter.
70
+ return [pid, name, bdate, sex].map((s) => s.replace('|', '_')).join('|');
71
+ }
72
+
73
+ interface State {
74
+ // volumeKey -> imageCacheMultiKey -> ITKImage
75
+ sliceData: Record<string, Record<string, object>>;
76
+
77
+ // volumeKey -> imageID
78
+ volumeToImageID: Record<string, string>;
79
+ // imageID -> volumeKey
80
+ imageIDToVolumeKey: Record<string, string>;
81
+
82
+ // volume invalidation information
83
+ needsRebuild: Record<string, boolean>;
84
+
85
+ // patientKey -> patient info
86
+ patientInfo: Record<string, PatientInfo>;
87
+ // patientKey -> array of studyKeys
88
+ patientStudies: Record<string, string[]>;
89
+
90
+ // studyKey -> study info
91
+ studyInfo: Record<string, StudyInfo>;
92
+ // studyKey -> array of volumeKeys
93
+ studyVolumes: Record<string, string[]>;
94
+
95
+ // volumeKey -> volume info
96
+ volumeInfo: Record<string, VolumeInfo>;
97
+
98
+ // parent pointers
99
+ // volumeKey -> studyKey
100
+ volumeStudy: Record<string, string>;
101
+ // studyKey -> patientKey
102
+ studyPatient: Record<string, string>;
103
+ }
104
+
105
+ export const useDICOMStore = defineStore('dicom', {
106
+ state: (): State => ({
107
+ sliceData: {},
108
+ volumeToImageID: {},
109
+ imageIDToVolumeKey: {},
110
+ patientInfo: {},
111
+ patientStudies: {},
112
+ studyInfo: {},
113
+ studyVolumes: {},
114
+ volumeInfo: {},
115
+ volumeStudy: {},
116
+ studyPatient: {},
117
+ needsRebuild: {},
118
+ }),
119
+ actions: {
120
+ async importFiles(files: File[]) {
121
+ const fileStore = useFileStore();
122
+
123
+ const dicomIO = this.$dicomIO;
124
+
125
+ if (!files.length) {
126
+ return [];
127
+ }
128
+
129
+ const nameToFileMap = new Map(files.map((f) => [f.name, f]));
130
+
131
+ const volumesToFilesMap = await dicomIO.categorizeFiles(files);
132
+ const updatedVolumeKeys: VolumeKeys[] = [];
133
+
134
+ await Promise.all(
135
+ Object.entries(volumesToFilesMap).map(
136
+ async ([volumeKey, fileNames]) => {
137
+ const volumeFiles: File[] = [];
138
+ fileNames.forEach((fileName) => {
139
+ const file = nameToFileMap.get(fileName);
140
+ if (file) {
141
+ volumeFiles.push(file);
142
+ }
143
+ });
144
+
145
+ // Add file entries to the file store
146
+ fileStore.add(volumeKey, volumeFiles);
147
+
148
+ // Now read the tags
149
+ const numberOfSlices = volumeFiles.length;
150
+
151
+ if (!(volumeKey in this.volumeInfo)) {
152
+ const info = await dicomIO.readTags(volumeFiles[0], [
153
+ { name: 'PatientName', tag: '0010|0010', strconv: true },
154
+ { name: 'PatientID', tag: '0010|0020', strconv: true },
155
+ { name: 'PatientBirthDate', tag: '0010|0030' },
156
+ { name: 'PatientSex', tag: '0010|0040' },
157
+ { name: 'StudyInstanceUID', tag: '0020|000d' },
158
+ { name: 'StudyDate', tag: '0008|0020' },
159
+ { name: 'StudyTime', tag: '0008|0030' },
160
+ { name: 'StudyID', tag: '0020|0010', strconv: true },
161
+ { name: 'AccessionNumber', tag: '0008|0050' },
162
+ { name: 'StudyDescription', tag: '0008|1030', strconv: true },
163
+ { name: 'Modality', tag: '0008|0060' },
164
+ { name: 'SeriesInstanceUID', tag: '0020|000e' },
165
+ { name: 'SeriesNumber', tag: '0020|0011' },
166
+ { name: 'SeriesDescription', tag: '0008|103e', strconv: true },
167
+ ]);
168
+
169
+ // TODO parse the raw string values
170
+ const patient = {
171
+ PatientID: info.PatientID || ANONYMOUS_PATIENT_ID,
172
+ PatientName: info.PatientName || ANONYMOUS_PATIENT,
173
+ PatientBirthDate: info.PatientBirthDate || '',
174
+ PatientSex: info.PatientSex || '',
175
+ };
176
+ const patientKey = genSynPatientKey(patient);
177
+
178
+ const studyKey = info.StudyInstanceUID;
179
+ const study = pick(
180
+ info,
181
+ 'StudyID',
182
+ 'StudyInstanceUID',
183
+ 'StudyDate',
184
+ 'StudyTime',
185
+ 'AccessionNumber',
186
+ 'StudyDescription'
187
+ );
188
+
189
+ const volumeInfo = {
190
+ ...pick(
191
+ info,
192
+ 'Modality',
193
+ 'SeriesInstanceUID',
194
+ 'SeriesNumber',
195
+ 'SeriesDescription'
196
+ ),
197
+ NumberOfSlices: numberOfSlices,
198
+ VolumeID: volumeKey,
199
+ };
200
+
201
+ updatedVolumeKeys.push({
202
+ patientKey,
203
+ studyKey,
204
+ volumeKey,
205
+ });
206
+
207
+ this._updateDatabase(patient, study, volumeInfo);
208
+ }
209
+
210
+ // invalidate any existing volume
211
+ if (volumeKey in this.volumeToImageID) {
212
+ // buildVolume requestor uses this as a rebuild hint
213
+ set(this.needsRebuild, volumeKey, true);
214
+ }
215
+ }
216
+ )
217
+ );
218
+
219
+ return updatedVolumeKeys;
220
+ },
221
+
222
+ _updateDatabase(
223
+ patient: PatientInfo,
224
+ study: StudyInfo,
225
+ volume: VolumeInfo
226
+ ) {
227
+ const patientKey = patient.PatientID;
228
+ const studyKey = study.StudyInstanceUID;
229
+ const volumeKey = volume.VolumeID;
230
+
231
+ if (!(patientKey in this.patientInfo)) {
232
+ set(this.patientInfo, patientKey, patient);
233
+ set(this.patientStudies, patientKey, []);
234
+ }
235
+
236
+ if (!(studyKey in this.studyInfo)) {
237
+ set(this.studyInfo, studyKey, study);
238
+ set(this.studyVolumes, studyKey, []);
239
+ set(this.studyPatient, studyKey, patientKey);
240
+ this.patientStudies[patientKey].push(studyKey);
241
+ }
242
+
243
+ if (!(volumeKey in this.volumeInfo)) {
244
+ set(this.volumeInfo, volumeKey, volume);
245
+ set(this.volumeStudy, volumeKey, studyKey);
246
+ set(this.sliceData, volumeKey, {});
247
+ this.studyVolumes[studyKey].push(volumeKey);
248
+ }
249
+ },
250
+
251
+ deleteVolume(volumeKey: string) {
252
+ const imageStore = useImageStore();
253
+ if (volumeKey in this.volumeInfo) {
254
+ const studyKey = this.volumeStudy[volumeKey];
255
+ del(this.volumeInfo, volumeKey);
256
+ del(this.sliceData, volumeKey);
257
+ del(this.volumeStudy, volumeKey);
258
+
259
+ if (volumeKey in this.volumeToImageID) {
260
+ const imageID = this.volumeToImageID[volumeKey];
261
+ imageStore.deleteData(imageID);
262
+ del(this.volumeToImageID, volumeKey);
263
+ del(this.imageIDToVolumeKey, imageID);
264
+ }
265
+
266
+ removeFromArray(this.studyVolumes[studyKey], volumeKey);
267
+ if (this.studyVolumes[studyKey].length === 0) {
268
+ this.deleteStudy(studyKey);
269
+ }
270
+ }
271
+ },
272
+
273
+ deleteStudy(studyKey: string) {
274
+ if (studyKey in this.studyInfo) {
275
+ const patientKey = this.studyPatient[studyKey];
276
+ del(this.studyInfo, studyKey);
277
+ del(this.studyPatient, studyKey);
278
+
279
+ [...this.studyVolumes[studyKey]].forEach((volumeKey) =>
280
+ this.deleteVolume(volumeKey)
281
+ );
282
+ del(this.studyVolumes, studyKey);
283
+
284
+ removeFromArray(this.patientStudies[patientKey], studyKey);
285
+ if (this.patientStudies[patientKey].length === 0) {
286
+ this.deletePatient(patientKey);
287
+ }
288
+ }
289
+ },
290
+
291
+ deletePatient(patientKey: string) {
292
+ if (patientKey in this.patientInfo) {
293
+ del(this.patientInfo, patientKey);
294
+
295
+ [...this.patientStudies[patientKey]].forEach((studyKey) =>
296
+ this.deleteStudy(studyKey)
297
+ );
298
+ del(this.patientStudies, patientKey);
299
+ }
300
+ },
301
+
302
+ async serialize(stateFile: StateFile) {
303
+ const dataIDs = Object.keys(this.volumeInfo);
304
+ await serializeData(stateFile, dataIDs, DataSetType.DICOM);
305
+ },
306
+
307
+ async deserialize(dataSet: DataSet, files: File[]) {
308
+ return this.importFiles(files).then((volumeKeys) => {
309
+ if (volumeKeys.length !== 1) {
310
+ // Volumes are store individually so we should get one back.
311
+ throw new Error('Invalid state file.');
312
+ }
313
+
314
+ return volumeKeys[0].volumeKey;
315
+ });
316
+ },
317
+
318
+ // returns an ITK image object
319
+ async getVolumeSlice(
320
+ volumeKey: string,
321
+ sliceIndex: number,
322
+ asThumbnail = false
323
+ ) {
324
+ const dicomIO = this.$dicomIO;
325
+ const fileStore = useFileStore();
326
+
327
+ const cacheKey = imageCacheMultiKey(sliceIndex, asThumbnail);
328
+ if (
329
+ volumeKey in this.sliceData &&
330
+ cacheKey in this.sliceData[volumeKey]
331
+ ) {
332
+ return this.sliceData[volumeKey][cacheKey];
333
+ }
334
+
335
+ if (!(volumeKey in this.volumeInfo)) {
336
+ throw new Error(`Cannot find given volume key: ${volumeKey}`);
337
+ }
338
+ const volumeInfo = this.volumeInfo[volumeKey];
339
+ const numSlices = volumeInfo.NumberOfSlices;
340
+
341
+ if (sliceIndex < 1 || sliceIndex > numSlices) {
342
+ throw new Error(`Slice ${sliceIndex} is out of bounds`);
343
+ }
344
+
345
+ const volumeFiles = fileStore.getFiles(volumeKey);
346
+
347
+ if (!volumeFiles) {
348
+ throw new Error(`No files found for volume key: ${volumeKey}`);
349
+ }
350
+
351
+ const sliceFile = volumeFiles[sliceIndex - 1];
352
+
353
+ const itkImage = dicomIO.getVolumeSlice(sliceFile, asThumbnail);
354
+
355
+ set(this.sliceData[volumeKey], cacheKey, itkImage);
356
+ return itkImage;
357
+ },
358
+
359
+ async buildVolume(volumeKey: string, forceRebuild: boolean = false) {
360
+ const imageStore = useImageStore();
361
+ const fileStore = useFileStore();
362
+ const dicomIO = this.$dicomIO;
363
+
364
+ const rebuild = forceRebuild || this.needsRebuild[volumeKey];
365
+
366
+ if (!rebuild && volumeKey in this.volumeToImageID) {
367
+ return imageStore.dataIndex[this.volumeToImageID[volumeKey]];
368
+ }
369
+
370
+ if (!(volumeKey in this.volumeInfo)) {
371
+ throw new Error(`Cannot find given volume key: ${volumeKey}`);
372
+ }
373
+
374
+ const volumeFiles = fileStore.getFiles(volumeKey);
375
+ if (!volumeFiles) {
376
+ throw new Error(`No files found for volume key: ${volumeKey}`);
377
+ }
378
+
379
+ const itkImage = await dicomIO.buildVolume(volumeFiles);
380
+ const vtkImage: vtkImageData =
381
+ vtkITKHelper.convertItkToVtkImage(itkImage);
382
+
383
+ if (volumeKey in this.volumeToImageID) {
384
+ const imageID = this.volumeToImageID[volumeKey];
385
+ imageStore.updateData(imageID, vtkImage);
386
+ } else {
387
+ const name = this.volumeInfo[volumeKey].SeriesInstanceUID;
388
+ const imageID = imageStore.addVTKImageData(name, vtkImage);
389
+ set(this.volumeToImageID, volumeKey, imageID);
390
+ set(this.imageIDToVolumeKey, imageID, volumeKey);
391
+ }
392
+
393
+ del(this.needsRebuild, volumeKey);
394
+
395
+ return vtkImage;
396
+ },
397
+ },
398
+ });
@@ -0,0 +1,35 @@
1
+ import { del, set } from '@vue/composition-api';
2
+ import { defineStore } from 'pinia';
3
+
4
+ interface State {
5
+ byDataID: Record<string, File[]>;
6
+ }
7
+
8
+ /**
9
+ * Store the File objects associated with a given dataset.
10
+ */
11
+ export const useFileStore = defineStore('files', {
12
+ state: (): State => ({
13
+ byDataID: {},
14
+ }),
15
+ getters: {
16
+ getFiles: (state) => {
17
+ return (dataID: string) => {
18
+ if (dataID in state.byDataID) {
19
+ return state.byDataID[dataID];
20
+ }
21
+ return null;
22
+ };
23
+ },
24
+ },
25
+ actions: {
26
+ remove(dataID: string) {
27
+ if (dataID in this.byDataID) {
28
+ del(this.byDataID, dataID);
29
+ }
30
+ },
31
+ add(dataID: string, files: File[]) {
32
+ set(this.byDataID, dataID, files);
33
+ },
34
+ },
35
+ });