diff --git a/package-lock.json b/package-lock.json index c90332843..221882d3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,14 @@ "@aws-sdk/client-s3": "^3.435.0", "@itk-wasm/dicom": "^5.0.0", "@itk-wasm/image-io": "^0.5.0", - "@kitware/vtk.js": "^28.13.0", + "@kitware/vtk.js": "^29.0.0", "@netlify/edge-functions": "^2.0.0", "@sentry/vue": "^7.54.0", - "@vueuse/core": "^8.5.0", - "@vueuse/shared": "^9.2.0", + "@vueuse/core": "^10.7.0", "core-js": "3.22.5", "deep-equal": "^2.0.5", "dicomweb-client-typed": "^0.8.6", + "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", "gl-matrix": "3.4.3", "itk-wasm": "1.0.0-b.156", @@ -3234,11 +3234,12 @@ } }, "node_modules/@kitware/vtk.js": { - "version": "28.13.0", - "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-28.13.0.tgz", - "integrity": "sha512-ZwJJ3Hp4SLssBd0yFJ04RVWutdqYfLHamBGt8uWs2j1SBAtZ75XPZ9c2PBqTGUAZKbmtRvucNpOKM8cFNQI1Eg==", + "version": "29.11.2", + "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-29.11.2.tgz", + "integrity": "sha512-SMYz7E5QlnMxg/iuNLYlae993EFG+eHjcQkGwWpGtPS2ANSnJdGiS0tCdc1LJz6gQ5LK01yZzObIHW472BybLQ==", "dependencies": { "@babel/runtime": "7.22.11", + "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", @@ -4805,9 +4806,14 @@ "dev": true }, "node_modules/@types/web-bluetooth": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz", - "integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==" + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "node_modules/@types/webxr": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.14.tgz", + "integrity": "sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==" }, "node_modules/@types/which": { "version": "2.0.2", @@ -5427,73 +5433,88 @@ } }, "node_modules/@vueuse/core": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz", - "integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz", + "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==", "dependencies": { - "@types/web-bluetooth": "^0.0.14", - "@vueuse/metadata": "8.9.4", - "@vueuse/shared": "8.9.4", - "vue-demi": "*" + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.7.2", + "@vueuse/shared": "10.7.2", + "vue-demi": ">=0.14.6" }, "funding": { "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.1.0", - "vue": "^2.6.0 || ^3.2.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - }, - "vue": { - "optional": true - } } }, - "node_modules/@vueuse/core/node_modules/@vueuse/shared": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz", - "integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==", - "dependencies": { - "vue-demi": "*" + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vue/composition-api": "^1.1.0", - "vue": "^2.6.0 || ^3.2.0" + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" }, "peerDependenciesMeta": { "@vue/composition-api": { "optional": true - }, - "vue": { - "optional": true } } }, "node_modules/@vueuse/metadata": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz", - "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz", + "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.5.0.tgz", - "integrity": "sha512-HnnCWU1Vg9CVWRCcI8ohDKDRB2Sc4bTgT1XAIaoLSfVHHn+TKbrox6pd3klCSw4UDxkhDfOk8cAdcK+Z5KleCA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz", + "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==", "dependencies": { - "vue-demi": "*" + "vue-demi": ">=0.14.6" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@wdio/cli": { "version": "8.32.3", "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.32.3.tgz", @@ -24581,11 +24602,12 @@ } }, "@kitware/vtk.js": { - "version": "28.13.0", - "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-28.13.0.tgz", - "integrity": "sha512-ZwJJ3Hp4SLssBd0yFJ04RVWutdqYfLHamBGt8uWs2j1SBAtZ75XPZ9c2PBqTGUAZKbmtRvucNpOKM8cFNQI1Eg==", + "version": "29.11.2", + "resolved": "https://registry.npmjs.org/@kitware/vtk.js/-/vtk.js-29.11.2.tgz", + "integrity": "sha512-SMYz7E5QlnMxg/iuNLYlae993EFG+eHjcQkGwWpGtPS2ANSnJdGiS0tCdc1LJz6gQ5LK01yZzObIHW472BybLQ==", "requires": { "@babel/runtime": "7.22.11", + "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", @@ -25880,9 +25902,14 @@ "dev": true }, "@types/web-bluetooth": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz", - "integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==" + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + }, + "@types/webxr": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.14.tgz", + "integrity": "sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==" }, "@types/which": { "version": "2.0.2", @@ -26346,37 +26373,43 @@ } }, "@vueuse/core": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz", - "integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz", + "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==", "requires": { - "@types/web-bluetooth": "^0.0.14", - "@vueuse/metadata": "8.9.4", - "@vueuse/shared": "8.9.4", - "vue-demi": "*" + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.7.2", + "@vueuse/shared": "10.7.2", + "vue-demi": ">=0.14.6" }, "dependencies": { - "@vueuse/shared": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz", - "integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==", - "requires": { - "vue-demi": "*" - } + "vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "requires": {} } } }, "@vueuse/metadata": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz", - "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==" + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz", + "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==" }, "@vueuse/shared": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.5.0.tgz", - "integrity": "sha512-HnnCWU1Vg9CVWRCcI8ohDKDRB2Sc4bTgT1XAIaoLSfVHHn+TKbrox6pd3klCSw4UDxkhDfOk8cAdcK+Z5KleCA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz", + "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==", "requires": { - "vue-demi": "*" + "vue-demi": ">=0.14.6" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "requires": {} + } } }, "@wdio/cli": { diff --git a/package.json b/package.json index 319416aec..8aaf7b253 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,14 @@ "@aws-sdk/client-s3": "^3.435.0", "@itk-wasm/dicom": "^5.0.0", "@itk-wasm/image-io": "^0.5.0", - "@kitware/vtk.js": "^28.13.0", + "@kitware/vtk.js": "^29.0.0", "@netlify/edge-functions": "^2.0.0", "@sentry/vue": "^7.54.0", - "@vueuse/core": "^8.5.0", - "@vueuse/shared": "^9.2.0", + "@vueuse/core": "^10.7.0", "core-js": "3.22.5", "deep-equal": "^2.0.5", "dicomweb-client-typed": "^0.8.6", + "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", "gl-matrix": "3.4.3", "itk-wasm": "1.0.0-b.156", @@ -126,4 +126,4 @@ "eslint" ] } -} +} \ No newline at end of file diff --git a/src/components/App.vue b/src/components/App.vue index 1ee1462b3..8414e41fc 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -49,7 +49,7 @@ + + + diff --git a/src/components/ObliqueSliceViewer.vue b/src/components/ObliqueSliceViewer.vue new file mode 100644 index 000000000..07433beb3 --- /dev/null +++ b/src/components/ObliqueSliceViewer.vue @@ -0,0 +1,271 @@ + + + + + + diff --git a/src/components/SliceSlider.vue b/src/components/SliceSlider.vue index d87f131c6..7708633a5 100644 --- a/src/components/SliceSlider.vue +++ b/src/components/SliceSlider.vue @@ -120,7 +120,7 @@ export default { Math.min(this.maxHandlePos, ev.pageY - y - this.handleHeight / 2) ); const newSlice = this.getNearestSlice(this.initialHandlePos); - this.$emit('modelValue', newSlice); + this.$emit('update:modelValue', newSlice); } this.yOffset = 0; @@ -134,7 +134,7 @@ export default { this.yOffset = ev.pageY - this.initialMousePosY; const slice = this.getNearestSlice(this.handlePosition); - this.$emit('modelValue', slice); + this.$emit('update:modelValue', slice); }, onDragEnd(ev) { @@ -144,7 +144,7 @@ export default { this.dragging = false; const slice = this.getNearestSlice(this.handlePosition); - this.$emit('modelValue', slice); + this.$emit('update:modelValue', slice); }, getNearestSlice(pos) { diff --git a/src/components/SliceViewer.vue b/src/components/SliceViewer.vue new file mode 100644 index 000000000..87086efe8 --- /dev/null +++ b/src/components/SliceViewer.vue @@ -0,0 +1,242 @@ + + + + + + diff --git a/src/components/SliceViewerOverlay.vue b/src/components/SliceViewerOverlay.vue new file mode 100644 index 000000000..822d54e95 --- /dev/null +++ b/src/components/SliceViewerOverlay.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/VolumeRendering.vue b/src/components/VolumeRendering.vue index 0e71385f2..44d84465a 100644 --- a/src/components/VolumeRendering.vue +++ b/src/components/VolumeRendering.vue @@ -15,6 +15,7 @@ import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/C import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy'; import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; +import useViewAnimationStore from '@/src/store/view-animation'; import { useResizeObserver } from '../composables/useResizeObserver'; import { useCurrentImage } from '../composables/useCurrentImage'; import useVolumeColoringStore from '../store/view-configs/volume-coloring'; @@ -23,8 +24,6 @@ import { getShiftedOpacityFromPreset, } from '../utils/vtk-helpers'; import { useVolumeThumbnailing } from '../composables/useVolumeThumbnailing'; -import { useViewProxy } from '../composables/useViewProxy'; -import { ViewProxyType } from '../core/proxies'; import { InitViewIDs } from '../config'; const WIDGET_WIDTH = 250; @@ -133,21 +132,21 @@ export default defineComponent({ onVTKEvent(pwfWidget, 'onOpacityChange', updateOpacityFunc); // trigger 3D view animations when updating the opacity widget - const { viewProxy } = useViewProxy(TARGET_VIEW_ID, ViewProxyType.Volume); + const viewAnimationStore = useViewAnimationStore(); let animationRequested = false; const request3DAnimation = () => { if (!animationRequested) { animationRequested = true; - viewProxy.value.getInteractor().requestAnimation(pwfWidget); + viewAnimationStore.requestAnimation(pwfWidget, { + byViewType: ['3D'], + }); } }; const cancel3DAnimation = () => { animationRequested = false; - viewProxy.value - .getInteractor() - .cancelAnimation(pwfWidget, true /* skipWarning */); + viewAnimationStore.cancelAnimation(pwfWidget); }; onVTKEvent(pwfWidget, 'onAnimation', (animating: boolean) => { diff --git a/src/components/VolumeViewer.vue b/src/components/VolumeViewer.vue new file mode 100644 index 000000000..4357cba9e --- /dev/null +++ b/src/components/VolumeViewer.vue @@ -0,0 +1,118 @@ + + + + + + + + diff --git a/src/components/VtkObliqueThreeView.vue b/src/components/VtkObliqueThreeView.vue deleted file mode 100644 index a0fea0e60..000000000 --- a/src/components/VtkObliqueThreeView.vue +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - - - diff --git a/src/components/VtkObliqueView.vue b/src/components/VtkObliqueView.vue deleted file mode 100644 index cf07da675..000000000 --- a/src/components/VtkObliqueView.vue +++ /dev/null @@ -1,576 +0,0 @@ - - - - - - - diff --git a/src/components/VtkThreeView.vue b/src/components/VtkThreeView.vue deleted file mode 100644 index 0e3b71f00..000000000 --- a/src/components/VtkThreeView.vue +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - - - diff --git a/src/components/VtkTwoView.vue b/src/components/VtkTwoView.vue deleted file mode 100644 index 32a1a42ef..000000000 --- a/src/components/VtkTwoView.vue +++ /dev/null @@ -1,621 +0,0 @@ - - - - - - diff --git a/src/components/tools/BoundingRectangle.vue b/src/components/tools/BoundingRectangle.vue index 5c01369c0..234fe9368 100644 --- a/src/components/tools/BoundingRectangle.vue +++ b/src/components/tools/BoundingRectangle.vue @@ -1,21 +1,20 @@ diff --git a/src/components/tools/MouseManipulatorTool.vue b/src/components/tools/MouseManipulatorTool.vue deleted file mode 100644 index 31d8f135d..000000000 --- a/src/components/tools/MouseManipulatorTool.vue +++ /dev/null @@ -1,111 +0,0 @@ - diff --git a/src/components/tools/PanTool.vue b/src/components/tools/PanTool.vue deleted file mode 100644 index dd31ff2a8..000000000 --- a/src/components/tools/PanTool.vue +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/src/components/tools/ResliceCursorTool.vue b/src/components/tools/ResliceCursorTool.vue index a8ad12f51..eeb4258c4 100644 --- a/src/components/tools/ResliceCursorTool.vue +++ b/src/components/tools/ResliceCursorTool.vue @@ -1,174 +1,101 @@ - diff --git a/src/components/tools/SelectTool.vue b/src/components/tools/SelectTool.vue index 5f8bc3809..f54d6d305 100644 --- a/src/components/tools/SelectTool.vue +++ b/src/components/tools/SelectTool.vue @@ -1,32 +1,22 @@ diff --git a/src/components/tools/ZoomTool.vue b/src/components/tools/ZoomTool.vue deleted file mode 100644 index c3abe4697..000000000 --- a/src/components/tools/ZoomTool.vue +++ /dev/null @@ -1,31 +0,0 @@ - diff --git a/src/components/tools/crop/Crop2D.vue b/src/components/tools/crop/Crop2D.vue index aa90aed88..495edcfd8 100644 --- a/src/components/tools/crop/Crop2D.vue +++ b/src/components/tools/crop/Crop2D.vue @@ -1,14 +1,12 @@ diff --git a/src/components/vtk/VtkBaseObliqueSliceRepresentation.vue b/src/components/vtk/VtkBaseObliqueSliceRepresentation.vue new file mode 100644 index 000000000..61eade9ba --- /dev/null +++ b/src/components/vtk/VtkBaseObliqueSliceRepresentation.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/vtk/VtkBaseSliceRepresentation.vue b/src/components/vtk/VtkBaseSliceRepresentation.vue new file mode 100644 index 000000000..1c98811c8 --- /dev/null +++ b/src/components/vtk/VtkBaseSliceRepresentation.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/vtk/VtkBaseVolumeRepresentation.vue b/src/components/vtk/VtkBaseVolumeRepresentation.vue new file mode 100644 index 000000000..599156838 --- /dev/null +++ b/src/components/vtk/VtkBaseVolumeRepresentation.vue @@ -0,0 +1,151 @@ + + + diff --git a/src/components/vtk/VtkImageOutlineRepresentation.vue b/src/components/vtk/VtkImageOutlineRepresentation.vue new file mode 100644 index 000000000..7a8a78da3 --- /dev/null +++ b/src/components/vtk/VtkImageOutlineRepresentation.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/components/vtk/VtkLayerSliceRepresentation.vue b/src/components/vtk/VtkLayerSliceRepresentation.vue new file mode 100644 index 000000000..0eb6ce844 --- /dev/null +++ b/src/components/vtk/VtkLayerSliceRepresentation.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/components/vtk/VtkMouseInteractionManipulator.vue b/src/components/vtk/VtkMouseInteractionManipulator.vue new file mode 100644 index 000000000..de7b5c7e0 --- /dev/null +++ b/src/components/vtk/VtkMouseInteractionManipulator.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/vtk/VtkOrientationMarker.vue b/src/components/vtk/VtkOrientationMarker.vue new file mode 100644 index 000000000..7bdf9fa13 --- /dev/null +++ b/src/components/vtk/VtkOrientationMarker.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/vtk/VtkSegmentationSliceRepresentation.vue b/src/components/vtk/VtkSegmentationSliceRepresentation.vue new file mode 100644 index 000000000..ab9807389 --- /dev/null +++ b/src/components/vtk/VtkSegmentationSliceRepresentation.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/components/vtk/VtkSliceView.vue b/src/components/vtk/VtkSliceView.vue new file mode 100644 index 000000000..2a0a2f54c --- /dev/null +++ b/src/components/vtk/VtkSliceView.vue @@ -0,0 +1,113 @@ + + + diff --git a/src/components/vtk/VtkSliceViewSlicingManipulator.vue b/src/components/vtk/VtkSliceViewSlicingManipulator.vue new file mode 100644 index 000000000..11c8fb910 --- /dev/null +++ b/src/components/vtk/VtkSliceViewSlicingManipulator.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/components/vtk/VtkSliceViewWindowManipulator.vue b/src/components/vtk/VtkSliceViewWindowManipulator.vue new file mode 100644 index 000000000..e546f8174 --- /dev/null +++ b/src/components/vtk/VtkSliceViewWindowManipulator.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/components/vtk/VtkVolumeView.vue b/src/components/vtk/VtkVolumeView.vue new file mode 100644 index 000000000..04a2cb146 --- /dev/null +++ b/src/components/vtk/VtkVolumeView.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/components/vtk/context.ts b/src/components/vtk/context.ts new file mode 100644 index 000000000..952cc96c6 --- /dev/null +++ b/src/components/vtk/context.ts @@ -0,0 +1,4 @@ +import { VtkViewApi } from '@/src/types/vtk-types'; +import { InjectionKey } from 'vue'; + +export const VtkViewContext: InjectionKey = Symbol('VtkView'); diff --git a/src/composables/annotationTool.ts b/src/composables/annotationTool.ts index 295a18aec..81e903585 100644 --- a/src/composables/annotationTool.ts +++ b/src/composables/annotationTool.ts @@ -3,12 +3,13 @@ import { Ref, UnwrapRef, computed, + onMounted, readonly, ref, unref, watch, } from 'vue'; -import { Vector2 } from '@kitware/vtk.js/types'; +import type { Vector2 } from '@kitware/vtk.js/types'; import { useCurrentImage } from '@/src/composables/useCurrentImage'; import { frameOfReferenceToImageSliceAndAxis } from '@/src/utils/frameOfReference'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; @@ -20,15 +21,14 @@ import { getCSSCoordinatesFromEvent } from '@/src//utils/vtk-helpers'; import { LPSAxis } from '@/src/types/lps'; import { AnnotationTool, ToolID } from '@/src/types/annotation-tool'; import vtkAbstractWidget from '@kitware/vtk.js/Widgets/Core/AbstractWidget'; -import { useViewStore } from '@/src/store/views'; -import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; import { usePopperState } from '@/src/composables/usePopperState'; -import { onViewProxyMounted } from '@/src/composables/useViewProxy'; import { ContextMenuEvent, vtkAnnotationToolWidget, } from '@/src/vtk/ToolWidgetUtils/types'; import { ImageMetadata } from '@/src/types/image'; +import { View } from '@/src/core/vtk/types'; +import { watchImmediate } from '@vueuse/core'; const SHOW_OVERLAY_DELAY = 250; // milliseconds @@ -89,7 +89,7 @@ export const useContextMenu = () => { export const useRightClickContextMenu = ( emit: (event: 'contextmenu', ...args: any[]) => void, - widget: Ref + widget: MaybeRef ) => { onVTKEvent(widget, 'onRightClickEvent', (eventData) => { const displayXY = getCSSCoordinatesFromEvent(eventData); @@ -106,7 +106,7 @@ export const useRightClickContextMenu = ( export const useHoverEvent = ( emit: (event: 'widgetHover', ...args: any[]) => void, - widget: Ref + widget: MaybeRef ) => { onVTKEvent(widget, 'onHoverEvent', (eventData: any) => { const displayXY = getCSSCoordinatesFromEvent(eventData); @@ -244,30 +244,23 @@ export const usePlacingAnnotationTool = ( }; export const useWidgetVisibility = ( - widget: Ref>, + widget: T, visible: Ref, - widgetManager: Ref>, - viewId: Ref + view: View ) => { // toggles the pickability of the ruler handles, // since the 3D ruler parts are visually hidden. - watch( - () => !!widget.value && visible.value, + watchImmediate( + () => visible.value, (visibility) => { - widget.value?.setVisibility(visibility); - }, - { immediate: true } + widget.setVisibility(visibility); + } ); - const viewProxy = computed(() => useViewStore().getViewProxy(viewId.value)); - - onViewProxyMounted(viewProxy, () => { - if (!widget.value) { - return; - } + onMounted(() => { // hide handle visibility, but not picking visibility - widget.value.setHandleVisibility(false); - widgetManager.value?.renderWidgets(); - viewProxy.value?.renderLater(); + widget.setHandleVisibility(false); + view.widgetManager.renderWidgets(); + view.requestRender(); }); }; diff --git a/src/composables/isViewAnimating.ts b/src/composables/isViewAnimating.ts index fc93dfd86..ddc052335 100644 --- a/src/composables/isViewAnimating.ts +++ b/src/composables/isViewAnimating.ts @@ -1,16 +1,14 @@ -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import { computed, ref, unref } from 'vue'; -import { MaybeRef } from '@vueuse/core'; +import { ref } from 'vue'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; +import { View } from '@/src/core/vtk/types'; -export function isViewAnimating(viewProxy: MaybeRef) { +export function isViewAnimating(view: View) { const isAnimating = ref(false); - const interactor = computed(() => unref(viewProxy).getInteractor()); - onVTKEvent(interactor, 'onStartAnimation', () => { + onVTKEvent(view.interactor, 'onStartAnimation', () => { isAnimating.value = true; }); - onVTKEvent(interactor, 'onEndAnimation', () => { + onVTKEvent(view.interactor, 'onEndAnimation', () => { isAnimating.value = false; }); diff --git a/src/composables/onPausableVTKEvent.ts b/src/composables/onPausableVTKEvent.ts new file mode 100644 index 000000000..f600db172 --- /dev/null +++ b/src/composables/onPausableVTKEvent.ts @@ -0,0 +1,50 @@ +import { vtkObject } from '@kitware/vtk.js/interfaces'; +import { MaybeRef } from 'vue'; +import { + OnVTKEventOptions, + VTKEventHandler, + VTKEventListener, + onVTKEvent, +} from './onVTKEvent'; + +export function onPausableVTKEvent( + vtkObj: MaybeRef, + eventHookName: T[K] extends VTKEventListener ? K : never, + callback: VTKEventHandler, + options?: OnVTKEventOptions +) { + let paused = false; + + const pause = () => { + paused = true; + }; + + const resume = () => { + paused = false; + }; + + const withPaused = (fn: () => void) => { + pause(); + try { + fn(); + } finally { + resume(); + } + }; + + const { stop } = onVTKEvent( + vtkObj, + eventHookName, + (obj) => { + if (!paused) callback(obj); + }, + options + ); + + return { + stop, + pause, + resume, + withPaused, + }; +} diff --git a/src/composables/onProxyManagerEvent.ts b/src/composables/onProxyManagerEvent.ts deleted file mode 100644 index dd0d743b8..000000000 --- a/src/composables/onProxyManagerEvent.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { vtkSubscription } from '@kitware/vtk.js/interfaces'; -import { VtkProxy } from '@kitware/vtk.js/macros'; -import { onBeforeUnmount } from 'vue'; -import { requireProxyManager } from './useProxyManager'; - -export type ProxyManagerEvent = - | 'ProxyCreated' - | 'ProxyModified' - | 'ProxyDeleted' - | 'ProxyRegistrationChange'; - -export function onProxyManagerEvent( - event: ProxyManagerEvent, - cb: ( - proxyID: string, - obj: VtkProxy | null, - action: 'register' | 'unregister' | 'modified' - ) => void -) { - const subs: vtkSubscription[] = []; - const proxyManager = requireProxyManager(); - - const proxySubs: Record = Object.create(null); - - subs.push( - proxyManager.onProxyRegistrationChange((info) => { - const { action, proxyId, proxy } = info; - if (action === 'register') { - if (event === 'ProxyCreated') { - cb(proxyId, proxy, 'register'); - } - if (event === 'ProxyModified') { - proxySubs[proxyId] = proxy.onModified(() => - cb(proxyId, proxy, 'modified') - ); - } - } else if (action === 'unregister') { - if (proxyId in proxySubs) { - proxySubs[proxyId].unsubscribe(); - delete proxySubs[proxyId]; - } - if (event === 'ProxyDeleted') { - cb(proxyId, null, 'unregister'); - } - } - if (event === 'ProxyRegistrationChange') { - cb(proxyId, proxy, action); - } - }) - ); - - onBeforeUnmount(() => { - while (subs.length) { - subs.pop()!.unsubscribe(); - } - Object.entries(proxySubs).forEach(([id, sub]) => { - sub.unsubscribe(); - delete proxySubs[id]; - }); - }); -} diff --git a/src/composables/onVTKEvent.ts b/src/composables/onVTKEvent.ts index 1d08a0a7a..32cad9c1c 100644 --- a/src/composables/onVTKEvent.ts +++ b/src/composables/onVTKEvent.ts @@ -1,6 +1,6 @@ import { Maybe } from '@/src/types'; import { vtkObject, vtkSubscription } from '@kitware/vtk.js/interfaces'; -import { MaybeRef, computed, onBeforeUnmount, unref, watch } from 'vue'; +import { MaybeRef, computed, onScopeDispose, unref, watch } from 'vue'; export type VTKEventHandler = (ev?: any) => any; export type VTKEventListener = ( @@ -23,15 +23,16 @@ export function onVTKEvent( }); let subscription: Maybe = null; - const stop = () => { + + const cleanup = () => { subscription?.unsubscribe(); subscription = null; }; - watch( + const stop = watch( listenerRef, (listener) => { - stop(); + cleanup(); if (listener) { subscription = listener(callback, options?.priority ?? 0); } @@ -39,7 +40,14 @@ export function onVTKEvent( { immediate: true } ); - onBeforeUnmount(() => { - stop(); + onScopeDispose(() => { + cleanup(); }); + + return { + stop: () => { + cleanup(); + stop(); + }, + }; } diff --git a/src/composables/stableDeepRef.ts b/src/composables/stableDeepRef.ts new file mode 100644 index 000000000..b8024d9ea --- /dev/null +++ b/src/composables/stableDeepRef.ts @@ -0,0 +1,27 @@ +import { Ref, computed, ref } from 'vue'; +import { watchCompare } from '@/src/utils/watchCompare'; +import deepEqual from 'fast-deep-equal'; + +/** + * Ensures that a Ref holds a stable reference by deep comparison. + * @param sourceRef + * @returns + */ +export function stableDeepRef(sourceRef: Ref) { + const stableRef = ref(sourceRef.value) as Ref; + watchCompare( + sourceRef, + (result) => { + stableRef.value = result; + }, + { compare: deepEqual } + ); + + return computed({ + get: () => stableRef.value, + set: (v) => { + // eslint-disable-next-line no-param-reassign + sourceRef.value = v; + }, + }); +} diff --git a/src/composables/useAutoFitState.ts b/src/composables/useAutoFitState.ts new file mode 100644 index 000000000..5a11ead06 --- /dev/null +++ b/src/composables/useAutoFitState.ts @@ -0,0 +1,13 @@ +import { onPausableVTKEvent } from '@/src/composables/onPausableVTKEvent'; +import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera'; +import { MaybeRef, ref } from 'vue'; + +export function useAutoFitState(camera: MaybeRef) { + const autoFit = ref(true); + + const { withPaused } = onPausableVTKEvent(camera, 'onModified', () => { + autoFit.value = false; + }); + + return { autoFit, withoutAutoFitEffect: withPaused }; +} diff --git a/src/composables/useBaseSliceRepresentation.ts b/src/composables/useBaseSliceRepresentation.ts deleted file mode 100644 index b61705dac..000000000 --- a/src/composables/useBaseSliceRepresentation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useProxyRepresentation } from '@/src/composables/useProxyRepresentations'; -import { Maybe } from '@/src/types'; -import vtkSliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/SliceRepresentationProxy'; -import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper'; -import { MaybeRef, watchEffect } from 'vue'; - -export function useBaseSliceRepresentation< - T extends vtkSliceRepresentationProxy = vtkSliceRepresentationProxy ->(imageID: MaybeRef>, viewID: MaybeRef) { - const { representation } = useProxyRepresentation(imageID, viewID); - - watchEffect(() => { - const rep = representation.value; - if (!rep) return; - - const mapper = rep.getMapper() as vtkImageMapper; - mapper.setResolveCoincidentTopologyToPolygonOffset(); - mapper.setResolveCoincidentTopologyPolygonOffsetParameters(1, 1); - }); - - return { representation }; -} diff --git a/src/composables/useCameraOrientation.ts b/src/composables/useCameraOrientation.ts index ff5c2e407..41ddf9baa 100644 --- a/src/composables/useCameraOrientation.ts +++ b/src/composables/useCameraOrientation.ts @@ -1,9 +1,10 @@ +import type { Vector3 } from '@kitware/vtk.js/types'; import { computed, Ref, unref } from 'vue'; import { MaybeRef } from '@vueuse/core'; import { mat3 } from 'gl-matrix'; -import { ImageMetadata } from '../types/image'; -import { LPSAxisDir } from '../types/lps'; -import { getLPSDirections } from '../utils/lps'; +import { ImageMetadata } from '@/src/types/image'; +import { LPSAxisDir } from '@/src/types/lps'; +import { getLPSDirections } from '@/src/utils/lps'; /** * @@ -23,9 +24,11 @@ export function useCameraOrientation( getLPSDirections(orientationMatrix.value) ); const cameraDirVec = computed( - () => lpsDirections.value[unref(viewDirection)] + () => lpsDirections.value[unref(viewDirection)] as Vector3 + ); + const cameraUpVec = computed( + () => lpsDirections.value[unref(viewUp)] as Vector3 ); - const cameraUpVec = computed(() => lpsDirections.value[unref(viewUp)]); return { cameraDirVec, diff --git a/src/composables/useColoringEffect.ts b/src/composables/useColoringEffect.ts index 6b2ea42d3..17ff87c00 100644 --- a/src/composables/useColoringEffect.ts +++ b/src/composables/useColoringEffect.ts @@ -1,102 +1,103 @@ -import { useProxyManager } from '@/src/composables/useProxyManager'; import { Maybe } from '@/src/types'; import { - ColorBy, ColorTransferFunction, ColoringConfig, OpacityFunction, } from '@/src/types/views'; -import vtkLPSView3DProxy from '@/src/vtk/LPSView3DProxy'; -import vtkVolumeRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/VolumeRepresentationProxy'; -import { Mode as LookupTableProxyMode } from '@kitware/vtk.js/Proxy/Core/LookupTableProxy'; -import { Ref, computed, watchEffect } from 'vue'; +import { MaybeRef, computed, unref, watchEffect } from 'vue'; import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy'; -import { getShiftedOpacityFromPreset } from '@/src/utils/vtk-helpers'; -import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy'; -import vtkProxyManager from '@kitware/vtk.js/Proxy/Core/ProxyManager'; +import { + applyNodesToPiecewiseFunction, + applyPointsToPiecewiseFunction, + getShiftedOpacityFromPreset, +} from '@/src/utils/vtk-helpers'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; +import vtkPiecewiseGaussianWidget from '@kitware/vtk.js/Interaction/Widgets/PiecewiseGaussianWidget'; export interface ApplyColoringParams { - colorBy: ColorBy; - colorFunc: ColorTransferFunction; - opacityFunc: OpacityFunction; - rep: vtkAbstractRepresentationProxy; - proxyManager: vtkProxyManager; + props: { + colorFunction: ColorTransferFunction; + opacityFunction: OpacityFunction; + }; + cfun: vtkColorTransferFunction; + ofun: vtkPiecewiseFunction; + componentIndex?: number; } export function applyColoring({ - colorBy, - colorFunc, - opacityFunc, - rep, - proxyManager, + props: { colorFunction, opacityFunction }, + cfun, + ofun, + componentIndex = -1, }: ApplyColoringParams) { - const { arrayName, location } = colorBy; + if (componentIndex === -1) { + cfun.setVectorModeToMagnitude(); + } else { + cfun.setVectorModeToComponent(); + cfun.setVectorComponent(componentIndex); + } - const lut = proxyManager.getLookupTable(arrayName); - lut.setMode(LookupTableProxyMode.Preset); - lut.setPresetName(colorFunc.preset); - lut.setDataRange(...colorFunc.mappingRange); + const preset = vtkColorMaps.getPresetByName(colorFunction.preset); + if (preset) { + cfun.applyColorMap(preset); + } + cfun.setMappingRange(...colorFunction.mappingRange); - const pwf = proxyManager.getPiecewiseFunction(arrayName); - pwf.setMode(opacityFunc.mode); - pwf.setDataRange(...opacityFunc.mappingRange); + const { mappingRange } = opacityFunction; + ofun.setRange(...opacityFunction.mappingRange); - switch (opacityFunc.mode) { + switch (opacityFunction.mode) { case vtkPiecewiseFunctionProxy.Mode.Gaussians: - pwf.setGaussians(opacityFunc.gaussians); + vtkPiecewiseGaussianWidget.applyGaussianToPiecewiseFunction( + opacityFunction.gaussians, + 256, + opacityFunction.mappingRange, + ofun + ); break; case vtkPiecewiseFunctionProxy.Mode.Points: { const opacityPoints = getShiftedOpacityFromPreset( - opacityFunc.preset, - opacityFunc.mappingRange, - opacityFunc.shift, - opacityFunc.shiftAlpha + opacityFunction.preset, + opacityFunction.mappingRange, + opacityFunction.shift, + opacityFunction.shiftAlpha ); if (opacityPoints) { - pwf.setPoints(opacityPoints); + applyPointsToPiecewiseFunction(ofun, opacityPoints, mappingRange); } break; } - case vtkPiecewiseFunctionProxy.Mode.Nodes: - pwf.setNodes(opacityFunc.nodes); + case vtkPiecewiseFunctionProxy.Mode.Nodes: { + applyNodesToPiecewiseFunction(ofun, opacityFunction.nodes, mappingRange); break; + } default: + throw new Error('Invalid opacity function mode encountered'); } - - // control color range manually - rep.setRescaleOnColorBy(false); - rep.setColorBy(arrayName, location); } export function useColoringEffect( - config: Ref>, - imageRep: Ref>, - viewProxy: Ref + config: MaybeRef>, + cfun: MaybeRef, + ofun: MaybeRef ) { - const colorBy = computed(() => config.value?.colorBy); - const colorTransferFunction = computed(() => config.value?.transferFunction); - const opacityFunction = computed(() => config.value?.opacityFunction); - - const proxyManager = useProxyManager(); + const colorTransferFunction = computed(() => unref(config)?.transferFunction); + const opacityFunction = computed(() => unref(config)?.opacityFunction); watchEffect(() => { - const rep = imageRep.value; - const colorBy_ = colorBy.value; const colorFunc = colorTransferFunction.value; const opacityFunc = opacityFunction.value; - if (!rep || !colorBy_ || !colorFunc || !opacityFunc || !proxyManager) { - return; - } + if (!colorFunc || !opacityFunc) return; applyColoring({ - colorBy: colorBy_, - colorFunc, - opacityFunc, - rep, - proxyManager, + props: { + colorFunction: colorFunc, + opacityFunction: opacityFunc, + }, + cfun: unref(cfun), + ofun: unref(ofun), }); - - // Need to trigger a render for when we are restoring from a state file - viewProxy.value.renderLater(); }); } diff --git a/src/composables/useCroppingEffect.ts b/src/composables/useCroppingEffect.ts new file mode 100644 index 000000000..134b24726 --- /dev/null +++ b/src/composables/useCroppingEffect.ts @@ -0,0 +1,20 @@ +import { croppingPlanesEqual } from '@/src/store/tools/crop'; +import { Maybe } from '@/src/types'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; +import { watchImmediate } from '@vueuse/core'; +import { MaybeRef, toRef } from 'vue'; + +export function useCroppingEffect( + mapper: vtkVolumeMapper, + planes: MaybeRef> +) { + // TODO make sure that the default planes are based off of spatial extent + watchImmediate(toRef(planes), (newPlanes, oldPlanes) => { + if (!newPlanes) return; + if (oldPlanes && croppingPlanesEqual(newPlanes, oldPlanes)) return; + + mapper.removeAllClippingPlanes(); + newPlanes.forEach((plane) => mapper.addClippingPlane(plane)); + }); +} diff --git a/src/composables/useCurrentSlice.ts b/src/composables/useCurrentSlice.ts deleted file mode 100644 index ec9c42634..000000000 --- a/src/composables/useCurrentSlice.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Vector3 } from '@kitware/vtk.js/types'; -import { computed, unref } from 'vue'; -import { MaybeRef } from '@vueuse/core'; -import { getLPSAxisFromDir } from '../utils/lps'; -import { useCurrentImage } from './useCurrentImage'; -import useViewSliceStore from '../store/view-configs/slicing'; - -/** - * Returns information about the current slice. - * - * axisName: the name of the axis - * axisIndex: corresponding index in an LPS coordinate array - * number: slice value - * planeNormal: slice plane normal - * planeOrigin: slice plane origin - * @param viewID - */ -export function useCurrentSlice(viewID: MaybeRef) { - const viewSliceStore = useViewSliceStore(); - const { currentImageMetadata, currentImageID } = useCurrentImage(); - return computed(() => { - const config = viewSliceStore.getConfig( - unref(viewID), - currentImageID.value - ); - if (!config) { - return null; - } - - const { lpsOrientation } = currentImageMetadata.value; - const axis = getLPSAxisFromDir(config.axisDirection); - const planeOrigin = [0, 0, 0] as Vector3; - planeOrigin[lpsOrientation[axis]] = config.slice; - return { - axisName: axis, - axisIndex: lpsOrientation[axis], - number: config.slice, - planeNormal: lpsOrientation[config.axisDirection] as Vector3, - planeOrigin, - }; - }); -} diff --git a/src/composables/useCvrEffect.ts b/src/composables/useCvrEffect.ts deleted file mode 100644 index 2da1aded3..000000000 --- a/src/composables/useCvrEffect.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { isViewAnimating } from '@/src/composables/isViewAnimating'; -import { VolumeColorConfig } from '@/src/store/view-configs/types'; -import { - DEFAULT_AMBIENT, - DEFAULT_DIFFUSE, - DEFAULT_SPECULAR, -} from '@/src/store/view-configs/volume-coloring'; -import { Maybe } from '@/src/types'; -import vtkLPSView3DProxy from '@/src/vtk/LPSView3DProxy'; -import { getDiagonalLength } from '@kitware/vtk.js/Common/DataModel/BoundingBox'; -import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; -import vtkVolumeRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/VolumeRepresentationProxy'; -import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; -import { Vector3 } from '@kitware/vtk.js/types'; -import { vec3 } from 'gl-matrix'; -import { Ref, computed, watch } from 'vue'; - -export function useCvrEffect( - config: Ref>, - imageRep: Ref>, - viewProxy: Ref -) { - const cvrParams = computed(() => config.value?.cvr); - const repMapper = computed( - () => imageRep.value?.getMapper() as Maybe - ); - const image = computed( - () => imageRep.value?.getInputDataSet() as Maybe - ); - const volume = computed(() => imageRep.value?.getVolumes()[0]); - const renderer = computed(() => viewProxy.value.getRenderer()); - const isAnimating = isViewAnimating(viewProxy); - const cvrEnabled = computed(() => { - const enabled = !!cvrParams.value?.enabled; - const animating = isAnimating.value; - return enabled && !animating; - }); - - // lights - const volumeCenter = computed(() => { - if (!volume.value) return null; - const volumeBounds = volume.value.getBounds(); - return [ - (volumeBounds[0] + volumeBounds[1]) / 2, - (volumeBounds[2] + volumeBounds[3]) / 2, - (volumeBounds[4] + volumeBounds[5]) / 2, - ] as Vector3; - }); - const lightFollowsCamera = computed( - () => cvrParams.value?.lightFollowsCamera ?? true - ); - - watch( - [volumeCenter, renderer, cvrEnabled, lightFollowsCamera], - ([center, ren, enabled, lightFollowsCamera_]) => { - if (!center) return; - - if (ren.getLights().length === 0) { - ren.createLight(); - } - const light = ren.getLights()[0]; - if (enabled) { - light.setFocalPoint(...center); - light.setColor(1, 1, 1); - light.setIntensity(1); - light.setConeAngle(90); - light.setPositional(true); - ren.setTwoSidedLighting(false); - if (lightFollowsCamera_) { - light.setLightTypeToHeadLight(); - ren.updateLightsGeometryToFollowCamera(); - } else { - light.setLightTypeToSceneLight(); - } - } else { - light.setPositional(false); - } - - viewProxy.value.renderLater(); - }, - { immediate: true } - ); - - // sampling distance - const volumeQuality = computed(() => cvrParams.value?.volumeQuality); - - watch( - [volume, image, repMapper, volumeQuality, cvrEnabled, isAnimating], - ([volume_, image_, mapper, volumeQuality_, enabled, animating]) => { - if (!volume_ || !mapper || volumeQuality_ == null || !image_) return; - - if (animating) { - mapper.setSampleDistance(0.75); - mapper.setMaximumSamplesPerRay(1000); - mapper.setGlobalIlluminationReach(0); - mapper.setComputeNormalFromOpacity(false); - } else { - const dims = image_.getDimensions(); - const spacing = image_.getSpacing(); - const spatialDiagonal = vec3.length( - vec3.fromValues( - dims[0] * spacing[0], - dims[1] * spacing[1], - dims[2] * spacing[2] - ) - ); - - // Use the average spacing for sampling by default - let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0; - // Adjust the volume sampling by the quality slider value - sampleDistance /= volumeQuality_ > 1 ? 0.5 * volumeQuality_ ** 2 : 1.0; - const samplesPerRay = spatialDiagonal / sampleDistance + 1; - mapper.setMaximumSamplesPerRay(samplesPerRay); - mapper.setSampleDistance(sampleDistance); - // Adjust the global illumination reach by volume quality slider - mapper.setGlobalIlluminationReach(enabled ? 0.25 * volumeQuality_ : 0); - mapper.setComputeNormalFromOpacity(!enabled && volumeQuality_ > 2); - } - - viewProxy.value.renderLater(); - }, - { immediate: true } - ); - - // volume properties - const ambient = computed(() => cvrParams.value?.ambient ?? 0); - const diffuse = computed(() => cvrParams.value?.diffuse ?? 0); - const specular = computed(() => cvrParams.value?.specular ?? 0); - - watch( - [volume, image, ambient, diffuse, specular, cvrEnabled], - ([volume_, image_, ambient_, diffuse_, specular_, enabled]) => { - if (!volume_ || !image_) return; - - const property = volume_.getProperty(); - property.setScalarOpacityUnitDistance( - 0, - (0.5 * getDiagonalLength(image_.getBounds())) / - Math.max(...image_.getDimensions()) - ); - - property.setShade(true); - property.setUseGradientOpacity(0, !enabled); - property.setGradientOpacityMinimumValue(0, 0.0); - const dataRange = image_.getPointData().getScalars().getRange(); - property.setGradientOpacityMaximumValue( - 0, - (dataRange[1] - dataRange[0]) * 0.01 - ); - property.setGradientOpacityMinimumOpacity(0, 0.0); - property.setGradientOpacityMaximumOpacity(0, 1.0); - - // do not toggle these parameters when animating - property.setAmbient(enabled ? ambient_ : DEFAULT_AMBIENT); - property.setDiffuse(enabled ? diffuse_ : DEFAULT_DIFFUSE); - property.setSpecular(enabled ? specular_ : DEFAULT_SPECULAR); - - viewProxy.value.renderLater(); - }, - { immediate: true } - ); - - // volumetric scattering blending - const useVolumetricScatteringBlending = computed( - () => cvrParams.value?.useVolumetricScatteringBlending ?? false - ); - const volumetricScatteringBlending = computed( - () => cvrParams.value?.volumetricScatteringBlending ?? 0 - ); - - watch( - [ - useVolumetricScatteringBlending, - volumetricScatteringBlending, - repMapper, - cvrEnabled, - ], - ([useVsb, vsb, mapper, enabled]) => { - if (!mapper) return; - - if (enabled && useVsb) { - mapper.setVolumetricScatteringBlending(vsb); - } else { - mapper.setVolumetricScatteringBlending(0); - } - - viewProxy.value.renderLater(); - }, - { immediate: true } - ); - - // local ambient occlusion - const useLocalAmbientOcclusion = computed( - () => cvrParams.value?.useLocalAmbientOcclusion ?? false - ); - const laoKernelSize = computed(() => cvrParams.value?.laoKernelSize ?? 0); - const laoKernelRadius = computed(() => cvrParams.value?.laoKernelRadius ?? 0); - - watch( - [ - useLocalAmbientOcclusion, - laoKernelSize, - laoKernelRadius, - repMapper, - cvrEnabled, - ], - ([useLao, kernelSize, kernelRadius, mapper, enabled]) => { - if (!mapper) return; - - if (enabled && useLao) { - mapper.setLocalAmbientOcclusion(true); - mapper.setLAOKernelSize(kernelSize); - mapper.setLAOKernelRadius(kernelRadius); - } else { - mapper.setLocalAmbientOcclusion(false); - mapper.setLAOKernelSize(0); - mapper.setLAOKernelRadius(0); - } - - viewProxy.value.renderLater(); - }, - { immediate: true } - ); -} diff --git a/src/composables/useFrameOfReference.ts b/src/composables/useFrameOfReference.ts index e16237e46..bfff6b30c 100644 --- a/src/composables/useFrameOfReference.ts +++ b/src/composables/useFrameOfReference.ts @@ -2,7 +2,7 @@ import { ImageMetadata } from '@/src/types/image'; import { LPSAxisDir } from '@/src/types/lps'; import { FrameOfReference } from '@/src/utils/frameOfReference'; import { getLPSAxisFromDir } from '@/src/utils/lps'; -import { Vector3 } from '@kitware/vtk.js/types'; +import type { Vector3 } from '@kitware/vtk.js/types'; import { vec3 } from 'gl-matrix'; import { computed, unref } from 'vue'; import type { ComputedRef, MaybeRef } from 'vue'; diff --git a/src/composables/useLabelMapRepresentations.ts b/src/composables/useLabelMapRepresentations.ts deleted file mode 100644 index 32e84433c..000000000 --- a/src/composables/useLabelMapRepresentations.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useProxyRepresentations } from '@/src/composables/useProxyRepresentations'; -import { Maybe } from '@/src/types'; -import vtkLabelMapSliceRepProxy from '@/src/vtk/LabelMapSliceRepProxy'; -import { MaybeRef } from 'vue'; - -export function useLabelMapRepresentations< - T extends vtkLabelMapSliceRepProxy = vtkLabelMapSliceRepProxy ->(dataIDs: MaybeRef>>>, viewID: MaybeRef) { - return useProxyRepresentations(dataIDs, viewID); -} diff --git a/src/composables/useLayerConfigInitializer.ts b/src/composables/useLayerConfigInitializer.ts new file mode 100644 index 000000000..08bb84b38 --- /dev/null +++ b/src/composables/useLayerConfigInitializer.ts @@ -0,0 +1,22 @@ +import { LayerID } from '@/src/store/datasets-layers'; +import useLayerColoringStore from '@/src/store/view-configs/layers'; +import { watchImmediate } from '@vueuse/core'; +import { MaybeRef, computed, unref } from 'vue'; + +export function useLayerConfigInitializer( + viewId: MaybeRef, + layerId: MaybeRef +) { + const coloringStore = useLayerColoringStore(); + const colorConfig = computed(() => + coloringStore.getConfig(unref(viewId), unref(layerId)) + ); + + watchImmediate(colorConfig, (config) => { + if (config) return; + + const viewIdVal = unref(viewId); + const layerIdVal = unref(layerId); + coloringStore.resetColorPreset(viewIdVal, layerIdVal); + }); +} diff --git a/src/composables/useLayerRepresentations.ts b/src/composables/useLayerRepresentations.ts deleted file mode 100644 index be1226e82..000000000 --- a/src/composables/useLayerRepresentations.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useProxyRepresentations } from '@/src/composables/useProxyRepresentations'; -import { Maybe } from '@/src/types'; -import vtkSliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/SliceRepresentationProxy'; -import { MaybeRef } from 'vue'; - -export function useLayerRepresentations< - T extends vtkSliceRepresentationProxy = vtkSliceRepresentationProxy ->(dataIDs: MaybeRef>>>, viewID: MaybeRef) { - return useProxyRepresentations(dataIDs, viewID); -} diff --git a/src/composables/useOrientationLabels.ts b/src/composables/useOrientationLabels.ts index 13737b490..c52cf1100 100644 --- a/src/composables/useOrientationLabels.ts +++ b/src/composables/useOrientationLabels.ts @@ -1,9 +1,10 @@ -import { computed, Ref, ref } from 'vue'; +import { computed, MaybeRef, ref, unref } from 'vue'; import { vec3 } from 'gl-matrix'; import type { Vector3 } from '@kitware/vtk.js/types'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; import { onVTKEvent } from '@/src/composables/onVTKEvent'; -import { EPSILON } from '../constants'; +import { EPSILON } from '@/src/constants'; +import { View } from '@/src/core/vtk/types'; +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; export function toOrderedLabels(vec: Vector3) { return ( @@ -27,8 +28,9 @@ export function toOrderedLabels(vec: Vector3) { * vtk.js coordinate coordinate is implied to be LPS, so orientation labels * are determined solely from the camera' direction and view-up. */ -export function useOrientationLabels(view: Ref) { - const camera = computed(() => view.value.getCamera()); +export function useOrientationLabels(view: MaybeRef) { + const renderer = computed(() => unref(view).renderer); + const camera = vtkFieldRef(renderer, 'activeCamera'); const top = ref(''); const left = ref(''); diff --git a/src/composables/usePersistCameraConfig.ts b/src/composables/usePersistCameraConfig.ts index 5bddbe127..424eeb219 100644 --- a/src/composables/usePersistCameraConfig.ts +++ b/src/composables/usePersistCameraConfig.ts @@ -1,139 +1,62 @@ -import { manageVTKSubscription } from '@/src/composables/manageVTKSubscription'; -import { Ref } from 'vue'; +import { MaybeRef, Ref, computed, unref } from 'vue'; import { Maybe } from '@/src/types'; -import { CameraConfig } from '../store/view-configs/types'; -import { vtkLPSViewProxy } from '../types/vtk-types'; -import useViewCameraStore from '../store/view-configs/camera'; +import { CameraConfig } from '@/src/store/view-configs/types'; +import useViewCameraStore from '@/src/store/view-configs/camera'; +import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera'; +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; +import { syncRef } from '@vueuse/core'; +import { guardedWritableRef } from '@/src/utils/guardedWritableRef'; export function usePersistCameraConfig( - viewID: Ref, - dataID: Ref>, - viewProxy: Ref, - ...toPersist: (keyof CameraConfig)[] + viewID: MaybeRef, + dataID: MaybeRef>, + camera: MaybeRef ) { const viewCameraStore = useViewCameraStore(); - let persistCameraConfig = true; - // We setup this list of functions to avoid if and indexOf in the onModified - // call. - const persist: (() => void)[] = []; + type KeyType = keyof CameraConfig; + const keys: KeyType[] = [ + 'position', + 'focalPoint', + 'viewUp', + 'parallelScale', + 'directionOfProjection', + ]; - if (toPersist.indexOf('position') > -1) { - persist.push(() => { - if (dataID.value != null && persistCameraConfig) { - viewCameraStore.updateConfig(viewID.value, dataID.value, { - position: viewProxy.value.getCamera().getPosition(), - }); - } - }); - } - if (toPersist.indexOf('viewUp') > -1) { - persist.push(() => { - if (dataID.value != null && persistCameraConfig) { - viewCameraStore.updateConfig(viewID.value, dataID.value, { - viewUp: viewProxy.value.getCamera().getViewUp(), - }); - } - }); - } - if (toPersist.indexOf('focalPoint') > -1) { - persist.push(() => { - if (dataID.value != null && persistCameraConfig) { - viewCameraStore.updateConfig(viewID.value, dataID.value, { - focalPoint: viewProxy.value.getCamera().getFocalPoint(), - }); - } - }); - } - if (toPersist.indexOf('directionOfProjection') > -1) { - persist.push(() => { - if (dataID.value != null && persistCameraConfig) { - viewCameraStore.updateConfig(viewID.value, dataID.value, { - directionOfProjection: viewProxy.value - .getCamera() - .getDirectionOfProjection(), - }); - } - }); - } - if (toPersist.indexOf('parallelScale') > -1) { - persist.push(() => { - if (dataID.value != null && persistCameraConfig) { - viewCameraStore.updateConfig(viewID.value, dataID.value, { - parallelScale: viewProxy.value.getCamera().getParallelScale(), - }); - } - }); - } - - manageVTKSubscription( - viewProxy.value.getCamera().onModified(() => { - persist.forEach((persistFunc) => persistFunc()); - }) + const cameraRefs = keys.reduce( + (refs, key) => ({ + ...refs, + [key]: guardedWritableRef( + vtkFieldRef(camera, key), + (incoming) => !!incoming + ), + }), + {} as Record> ); - function blockPersistingCameraConfig(func: () => void) { - persistCameraConfig = false; - try { - func(); - } finally { - persistCameraConfig = true; - } - } + const config = computed(() => + viewCameraStore.getConfig(unref(viewID), unref(dataID)) + ); - function restoreCameraConfig(cameraConfig: CameraConfig) { - blockPersistingCameraConfig(() => { - toPersist.forEach((key: keyof CameraConfig) => { - // Parallel scale - if (key === 'parallelScale' && cameraConfig.parallelScale) { - viewProxy.value - .getCamera() - .setParallelScale(cameraConfig.parallelScale); - } - // Position - else if (key === 'position' && cameraConfig.position) { - const { position } = cameraConfig; - viewProxy.value - .getCamera() - .setPosition(position[0], position[1], position[2]); - } - // Focal point - else if (key === 'focalPoint' && cameraConfig.focalPoint) { - const { focalPoint } = cameraConfig; - viewProxy.value - .getCamera() - .setFocalPoint(focalPoint[0], focalPoint[1], focalPoint[2]); - viewProxy.value - .getInteractorStyle2D() - .setCenterOfRotation([...focalPoint]); - viewProxy.value - .getInteractorStyle3D() - .setCenterOfRotation([...focalPoint]); - } - // Direction of projection - else if ( - key === 'directionOfProjection' && - cameraConfig.directionOfProjection - ) { - const { directionOfProjection } = cameraConfig; - viewProxy.value - .getCamera() - .setDirectionOfProjection( - directionOfProjection[0], - directionOfProjection[1], - directionOfProjection[2] - ); - } - // View up - else if (key === 'viewUp' && cameraConfig.viewUp) { - const { viewUp } = cameraConfig; - viewProxy.value - .getCamera() - .setViewUp(viewUp[0], viewUp[1], viewUp[2]); - } - }); - }); - } + const configRefs = keys.reduce( + (refs, key) => ({ + ...refs, + [key]: computed({ + get: () => config.value?.[key], + set: (v) => { + const viewIDVal = unref(viewID); + const dataIDVal = unref(dataID); + if (!viewIDVal || !dataIDVal) return; + viewCameraStore.updateConfig(viewIDVal, dataIDVal, { + [key]: v, + }); + }, + }), + }), + {} as Record> + ); - return { restoreCameraConfig }; + keys.forEach((key) => { + syncRef(configRefs[key], cameraRefs[key]); + }); } diff --git a/src/composables/useProxyManager.ts b/src/composables/useProxyManager.ts deleted file mode 100644 index 640ca4a02..000000000 --- a/src/composables/useProxyManager.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { inject } from 'vue'; -import ProxyManagerWrapper from '@/src/core/proxies'; - -export const PROXY_MANAGER_WRAPPER_INJECT_KEY = Symbol('ProxyManagerWrapper'); - -/** - * Injects a provided ProxyManagerWrapper instance if available. - */ -export function useProxyManagerWrapper() { - return inject(PROXY_MANAGER_WRAPPER_INJECT_KEY); -} - -/** - * Injects a provided ProxyManagerWrapper instance. - * - * Throws an error if none is available. - */ -export function requireProxyManagerWrapper() { - const pxm = useProxyManagerWrapper(); - if (!pxm) throw new Error('No ProxyManagerWrapper provided'); - return pxm; -} - -/** - * Obtains the underlying vtkProxyManager. - */ -export function useProxyManager() { - return useProxyManagerWrapper()?.proxyManager; -} - -/** - * Obtains the underlying vtkProxyManager. - * - * Throws an error if none is available. - */ -export function requireProxyManager() { - const pxm = useProxyManager(); - if (!pxm) throw new Error('No ProxyManager provided'); - return pxm; -} diff --git a/src/composables/useProxyRepresentations.ts b/src/composables/useProxyRepresentations.ts deleted file mode 100644 index 95ec64364..000000000 --- a/src/composables/useProxyRepresentations.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useViewStore } from '@/src/store/views'; -import { Maybe } from '@/src/types'; -import { vtkLPSViewProxy } from '@/src/types/vtk-types'; -import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy'; -import { MaybeRef, computed, unref, watchPostEffect } from 'vue'; - -export function useProxyRepresentations< - T extends vtkAbstractRepresentationProxy = vtkAbstractRepresentationProxy ->(dataIDs: MaybeRef>>>, viewID: MaybeRef) { - const viewStore = useViewStore(); - const viewProxy = computed(() => - viewStore.getViewProxy(unref(viewID)) - ); - - const representations = computed(() => { - const viewIdVal = unref(viewID); - return (unref(dataIDs) ?? []) - .filter((id): id is string => !!id) - .map((id) => { - return viewStore.getDataRepresentationForView(id, viewIdVal); - }) - .filter((rep): rep is T => !!rep); - }); - - // Wait for the component to have mounted with PostEffect. - // When changing layouts, it's possible for the new component instance to - // execute effects prior to the old component running onCleanup callbacks. - watchPostEffect((onCleanup) => { - const view = viewProxy.value; - if (!view) return; - - const reps = representations.value; - if (!reps.length) return; - - reps.forEach((rep) => { - view.addRepresentation(rep); - }); - view.getRenderer().computeVisiblePropBounds(); - view.renderLater(); - - onCleanup(() => { - reps.forEach((rep) => { - view.removeRepresentation(rep); - }); - }); - }); - - return { representations }; -} - -export function useProxyRepresentation< - T extends vtkAbstractRepresentationProxy = vtkAbstractRepresentationProxy ->(dataID: MaybeRef>, viewID: MaybeRef) { - const ids = computed(() => [unref(dataID)]); - const { representations } = useProxyRepresentations(ids, viewID); - const representation = computed>( - () => representations.value[0] ?? null - ); - return { representation }; -} diff --git a/src/composables/useSliceConfig.ts b/src/composables/useSliceConfig.ts index 7e21d9c35..be82b526a 100644 --- a/src/composables/useSliceConfig.ts +++ b/src/composables/useSliceConfig.ts @@ -2,7 +2,7 @@ import useViewSliceStore, { defaultSliceConfig, } from '@/src/store/view-configs/slicing'; import { Maybe } from '@/src/types'; -import { Vector2 } from '@kitware/vtk.js/types'; +import type { Vector2 } from '@kitware/vtk.js/types'; import { unref, MaybeRef, computed } from 'vue'; export function useSliceConfig( diff --git a/src/composables/useSliceConfigInitializer.ts b/src/composables/useSliceConfigInitializer.ts index e3fbcbaa4..590070b56 100644 --- a/src/composables/useSliceConfigInitializer.ts +++ b/src/composables/useSliceConfigInitializer.ts @@ -1,40 +1,50 @@ +import { useImage } from '@/src/composables/useCurrentImage'; import { useSliceConfig } from '@/src/composables/useSliceConfig'; import useViewSliceStore from '@/src/store/view-configs/slicing'; import { Maybe } from '@/src/types'; import { LPSAxisDir } from '@/src/types/lps'; -import { MaybeRef, toRef, unref, watch } from 'vue'; +import { getLPSAxisFromDir } from '@/src/utils/lps'; +import type { vtkRange } from '@kitware/vtk.js/interfaces'; +import { watchImmediate } from '@vueuse/core'; +import { MaybeRef, computed, toRef, unref } from 'vue'; export function useSliceConfigInitializer( viewID: MaybeRef, imageID: MaybeRef>, viewDirection: MaybeRef, - sliceDomain: MaybeRef<{ min: number; max: number }> + slicingDomain?: MaybeRef ) { const store = useViewSliceStore(); const { config: sliceConfig } = useSliceConfig(viewID, imageID); + const { metadata } = useImage(imageID); - watch( - toRef(sliceDomain), - (domain) => { - const imageIdVal = unref(imageID); - if (!imageIdVal) return; - store.updateConfig(unref(viewID), imageIdVal, domain); - }, - { immediate: true } - ); + const viewAxis = computed(() => getLPSAxisFromDir(unref(viewDirection))); + const sliceDomain = computed(() => { + const domainArg = unref(slicingDomain); + if (domainArg) return domainArg; + const { lpsOrientation, dimensions } = metadata.value; + const ijkIndex = lpsOrientation[viewAxis.value]; + const dimMax = dimensions[ijkIndex]; - watch( - sliceConfig, - (config) => { + return { + min: 0, + max: dimMax - 1, + }; + }); + + watchImmediate( + [toRef(sliceDomain), toRef(viewDirection)] as const, + ([domain, axisDirection]) => { + const configExisted = !!sliceConfig.value; const imageIdVal = unref(imageID); - const viewIdVal = unref(viewID); - if (config || !imageIdVal) return; - store.updateConfig(viewIdVal, imageIdVal, { - ...unref(sliceDomain), - axisDirection: unref(viewDirection), + if (!imageIdVal) return; + store.updateConfig(unref(viewID), imageIdVal, { + ...domain, + axisDirection, }); - store.resetSlice(viewIdVal, imageIdVal); - }, - { immediate: true } + if (!configExisted) { + store.resetSlice(unref(viewID), imageIdVal); + } + } ); } diff --git a/src/composables/useSliceInfo.ts b/src/composables/useSliceInfo.ts new file mode 100644 index 000000000..3a42fe1fc --- /dev/null +++ b/src/composables/useSliceInfo.ts @@ -0,0 +1,40 @@ +import type { Vector3 } from '@kitware/vtk.js/types'; +import { computed } from 'vue'; +import { MaybeRef } from '@vueuse/core'; +import { getLPSAxisFromDir } from '@/src/utils/lps'; +import { useImage } from '@/src/composables/useCurrentImage'; +import { Maybe } from '@/src/types'; +import { useSliceConfig } from '@/src/composables/useSliceConfig'; + +/** + * Returns information about the current slice. + * + * axisName: the name of the axis + * axisIndex: corresponding index in an LPS coordinate array + * number: slice value + * planeNormal: slice plane normal + * planeOrigin: slice plane origin + * @param viewID + */ +export function useSliceInfo( + viewID: MaybeRef, + imageID: MaybeRef> +) { + const { metadata: imageMetadata } = useImage(imageID); + const { slice, config } = useSliceConfig(viewID, imageID); + return computed(() => { + if (!config.value) return null; + const { lpsOrientation } = imageMetadata.value; + const { axisDirection } = config.value; + const axis = getLPSAxisFromDir(axisDirection); + const planeOrigin = [0, 0, 0] as Vector3; + planeOrigin[lpsOrientation[axis]] = slice.value; + return { + axisName: axis, + axisIndex: lpsOrientation[axis], + slice: slice.value, + planeNormal: lpsOrientation[axisDirection] as Vector3, + planeOrigin, + }; + }); +} diff --git a/src/composables/useViewAnimationListener.ts b/src/composables/useViewAnimationListener.ts new file mode 100644 index 000000000..3f49941a3 --- /dev/null +++ b/src/composables/useViewAnimationListener.ts @@ -0,0 +1,34 @@ +import { View } from '@/src/core/vtk/types'; +import useViewAnimationStore, { + matchesViewFilter, +} from '@/src/store/view-animation'; +import { Maybe } from '@/src/types'; +import { storeToRefs } from 'pinia'; +import { MaybeRef, computed, unref, watchEffect } from 'vue'; + +export function useViewAnimationListener( + view: MaybeRef>, + viewId: MaybeRef, + viewType: MaybeRef +) { + const store = useViewAnimationStore(); + const { animating, viewFilter } = storeToRefs(store); + const canAnimate = computed(() => + matchesViewFilter(unref(viewId), unref(viewType), viewFilter.value) + ); + + let requested = false; + + watchEffect(() => { + const viewVal = unref(view); + if (!viewVal) return; + + if (!animating.value) { + viewVal.interactor.cancelAnimation(store); + requested = false; + } else if (!requested && canAnimate.value) { + viewVal.interactor.requestAnimation(store); + requested = true; + } + }); +} diff --git a/src/composables/useViewProxy.ts b/src/composables/useViewProxy.ts deleted file mode 100644 index 0b25af4f3..000000000 --- a/src/composables/useViewProxy.ts +++ /dev/null @@ -1,82 +0,0 @@ -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import { computed, onUnmounted, ref, unref, watch, watchEffect } from 'vue'; -import { MaybeRef, useElementSize } from '@vueuse/core'; -import { onVTKEvent } from '@/src/composables/onVTKEvent'; -import { Maybe } from '@/src/types'; -import { ViewProxyType } from '../core/proxies'; -import { useViewStore } from '../store/views'; - -export function useViewProxy( - id: MaybeRef, - type: MaybeRef -) { - const viewStore = useViewStore(); - - const viewProxy = computed(() => - viewStore.createOrGetViewProxy(unref(id), unref(type)) - ); - - return { - viewProxy, - }; -} - -function isViewProxyMounted( - viewProxy: MaybeRef> -) { - const mounted = ref(false); - - const container = ref>(unref(viewProxy)?.getContainer()); - onVTKEvent(viewProxy, 'onModified', () => { - container.value = unref(viewProxy)?.getContainer(); - }); - - const { width, height } = useElementSize(container); - - const updateMounted = () => { - // view is considered mounted when the container has a non-zero size - mounted.value = !!(width.value && height.value); - }; - - watchEffect(() => updateMounted()); - - return mounted; -} - -export function onViewProxyMounted( - viewProxy: MaybeRef>, - callback: () => void -) { - const mounted = isViewProxyMounted(viewProxy); - - watch( - mounted, - (m) => { - if (m) callback(); - }, - { immediate: true } - ); -} - -export function onViewProxyUnmounted( - viewProxy: MaybeRef>, - callback: () => void -) { - const mounted = isViewProxyMounted(viewProxy); - let invoked = false; - const invokeCallback = () => { - if (invoked) return; - callback(); - invoked = true; - }; - - onUnmounted(() => invokeCallback()); - - watch( - mounted, - (m, prev) => { - if (prev && !m) invokeCallback(); - }, - { immediate: true } - ); -} diff --git a/src/composables/useVolumeColoringInitializer.ts b/src/composables/useVolumeColoringInitializer.ts new file mode 100644 index 000000000..4c176893f --- /dev/null +++ b/src/composables/useVolumeColoringInitializer.ts @@ -0,0 +1,27 @@ +import { useImage } from '@/src/composables/useCurrentImage'; +import useVolumeColoringStore from '@/src/store/view-configs/volume-coloring'; +import { Maybe } from '@/src/types'; +import { watchImmediate } from '@vueuse/core'; +import { MaybeRef, computed, unref } from 'vue'; + +export function useVolumeColoringInitializer( + viewId: MaybeRef, + imageId: MaybeRef> +) { + const store = useVolumeColoringStore(); + const coloringConfig = computed(() => + store.getConfig(unref(viewId), unref(imageId)) + ); + + const { imageData } = useImage(imageId); + + watchImmediate(coloringConfig, (config) => { + if (config) return; + + const viewIdVal = unref(viewId); + const imageIdVal = unref(imageId); + if (!imageIdVal || !imageData.value) return; + + store.resetToDefaultColoring(viewIdVal, imageIdVal, imageData.value); + }); +} diff --git a/src/composables/useWebGLWatchdog.ts b/src/composables/useWebGLWatchdog.ts index 6142be663..d7e242f15 100644 --- a/src/composables/useWebGLWatchdog.ts +++ b/src/composables/useWebGLWatchdog.ts @@ -1,24 +1,20 @@ import { captureMessage } from '@sentry/vue'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import vtkProxyManager from '@kitware/vtk.js/Proxy/Core/ProxyManager'; import { useEventListener, useThrottleFn } from '@vueuse/core'; +import { Messages } from '@/src/constants'; +import { useMessageStore } from '@/src/store/messages'; +import { View } from '@/src/core/vtk/types'; +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; +import { MaybeRef, computed, unref } from 'vue'; import { Maybe } from '@/src/types'; -import { useProxyManager } from '@/src/composables/useProxyManager'; -import { Messages } from '../constants'; -import { useMessageStore } from '../store/messages'; -import { onProxyManagerEvent } from './onProxyManagerEvent'; + +const THROTTLE_THRESHOLD = 250; // ms /** * Collects relevant context for debugging 3D crashes. * @returns */ -function getVolumeMapperContext(pxm: Maybe) { - if (!pxm) return null; - - const view3d = pxm.getViews().find((view) => view.isA('vtkLPSView3DProxy')); - if (!view3d) return null; - - const ren = view3d.getRenderer(); +function getVolumeMapperContext(view: View) { + const ren = view.renderer; const vol = ren.getVolumes()[0]; if (!vol) return null; @@ -34,39 +30,21 @@ function getVolumeMapperContext(pxm: Maybe) { ); } -export function useWebGLWatchdog() { - const watchdogs = new Map void>(); - const pxm = useProxyManager(); - +export function useWebGLWatchdog(view: MaybeRef>) { const reportError = useThrottleFn(() => { const messageStore = useMessageStore(); messageStore.addError(Messages.WebGLLost.title, Messages.WebGLLost.details); - captureMessage('WebGL2 context was lost', { - contexts: { - vtk: { - volumeMapper: getVolumeMapperContext(pxm), - }, - }, - }); - }, 150); - onProxyManagerEvent('ProxyCreated', (id, obj) => { - if (!obj || !obj.isA('vtkViewProxy')) return; - const view = obj as vtkViewProxy; - // TODO getCanvas() typing - const canvas = view - .getRenderWindow() - .getViews()[0] - .getCanvas() as HTMLCanvasElement; + const contexts: Record = {}; + const viewVal = unref(view); + if (viewVal) { + contexts.vtk = getVolumeMapperContext(viewVal); + } - const cleanup = useEventListener(canvas, 'webglcontextlost', reportError); - watchdogs.set(id, cleanup); - }); + captureMessage('WebGL2 context was lost', { contexts }); + }, THROTTLE_THRESHOLD); - onProxyManagerEvent('ProxyDeleted', (id) => { - if (watchdogs.has(id)) { - watchdogs.get(id)!(); - watchdogs.delete(id); - } - }); + const renWinView = computed(() => unref(view)?.renderWindowView); + const canvas = vtkFieldRef(renWinView, 'canvas'); + useEventListener(canvas, 'webglcontextlost', reportError); } diff --git a/src/composables/useWidgetManager.ts b/src/composables/useWidgetManager.ts deleted file mode 100644 index f27bb17ab..000000000 --- a/src/composables/useWidgetManager.ts +++ /dev/null @@ -1,41 +0,0 @@ -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; -import { CaptureOn } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants'; -import { computed, onUnmounted, Ref, watch } from 'vue'; -import { onViewProxyMounted, onViewProxyUnmounted } from './useViewProxy'; - -export function useWidgetManager(viewProxy: Ref) { - const widgetManager = computed(() => { - const wm = vtkWidgetManager.newInstance({ - pickingEnabled: false, - useSvgLayer: false, - captureOn: CaptureOn.MOUSE_MOVE, - }); - return wm; - }); - - onViewProxyMounted(viewProxy, () => { - widgetManager.value.setRenderer(viewProxy.value.getRenderer()); - widgetManager.value.enablePicking(); - }); - - onViewProxyUnmounted(viewProxy, () => { - widgetManager.value.disablePicking(); - }); - - watch(widgetManager, (curWM, oldWM) => { - if (curWM) { - curWM.setRenderer(viewProxy.value.getRenderer()); - curWM.enablePicking(); - } - if (oldWM) { - oldWM.delete(); - } - }); - - onUnmounted(() => { - widgetManager.value.delete(); - }); - - return { widgetManager }; -} diff --git a/src/composables/useWindowingConfig.ts b/src/composables/useWindowingConfig.ts index c3586bb0b..91733f15b 100644 --- a/src/composables/useWindowingConfig.ts +++ b/src/composables/useWindowingConfig.ts @@ -1,6 +1,6 @@ import useWindowingStore from '@/src/store/view-configs/windowing'; import { Maybe } from '@/src/types'; -import { Vector2 } from '@kitware/vtk.js/types'; +import type { Vector2 } from '@kitware/vtk.js/types'; import { MaybeRef, unref, computed } from 'vue'; export function useWindowingConfig( @@ -12,7 +12,7 @@ export function useWindowingConfig( const generateComputed = (prop: 'width' | 'level') => { return computed({ - get: () => config.value?.[prop], + get: () => config.value?.[prop] ?? 0, set: (val) => { const imageIdVal = unref(imageID); if (!imageIdVal || val == null) return; diff --git a/src/composables/useWindowingConfigInitializer.ts b/src/composables/useWindowingConfigInitializer.ts index 79cfee1aa..7174e9802 100644 --- a/src/composables/useWindowingConfigInitializer.ts +++ b/src/composables/useWindowingConfigInitializer.ts @@ -4,7 +4,8 @@ import { WLAutoRanges, WL_AUTO_DEFAULT, WL_HIST_BINS } from '@/src/constants'; import { useDICOMStore } from '@/src/store/datasets-dicom'; import useWindowingStore from '@/src/store/view-configs/windowing'; import { Maybe } from '@/src/types'; -import { TypedArray } from '@kitware/vtk.js/types'; +import type { TypedArray } from '@kitware/vtk.js/types'; +import { watchImmediate } from '@vueuse/core'; import { MaybeRef, computed, unref, watch } from 'vue'; function useAutoRangeValues(imageID: MaybeRef>) { @@ -83,50 +84,40 @@ export function useWindowingConfigInitializer( return {}; }); - watch( - windowConfig, - (config) => { - const image = imageData.value; - const imageIdVal = unref(imageID); - const viewIdVal = unref(viewID); - if (config || !image || !imageIdVal) return; + watchImmediate(windowConfig, (config) => { + const image = imageData.value; + const imageIdVal = unref(imageID); + const viewIdVal = unref(viewID); + if (config || !image || !imageIdVal) return; - const [min, max] = image.getPointData().getScalars().getRange(); - store.updateConfig(viewIdVal, imageIdVal, { min, max }); - store.resetWindowLevel(viewIdVal, imageIdVal); - }, - { immediate: true } - ); + const [min, max] = image.getPointData().getScalars().getRange(); + store.updateConfig(viewIdVal, imageIdVal, { min, max }); + store.resetWindowLevel(viewIdVal, imageIdVal); + }); - watch( - imageData, - (image) => { - const imageIdVal = unref(imageID); - const config = unref(windowConfig); - const viewIdVal = unref(viewID); - if (imageIdVal == null || config != null || !image) { - return; - } + watchImmediate(imageData, (image) => { + const imageIdVal = unref(imageID); + const config = unref(windowConfig); + const viewIdVal = unref(viewID); + if (imageIdVal == null || config != null || !image) { + return; + } - const range = autoRangeValues.value[autoRange.value]; + const range = autoRangeValues.value[autoRange.value]; + store.updateConfig(viewIdVal, imageIdVal, { + min: range[0], + max: range[1], + }); + if (firstTag.value?.width) { store.updateConfig(viewIdVal, imageIdVal, { - min: range[0], - max: range[1], + preset: { + width: parseFloat(firstTag.value.width), + level: parseFloat(firstTag.value.level), + }, }); - if (firstTag.value?.width) { - store.updateConfig(viewIdVal, imageIdVal, { - preset: { - width: parseFloat(firstTag.value.width), - level: parseFloat(firstTag.value.level), - }, - }); - } - store.resetWindowLevel(viewIdVal, imageIdVal); - }, - { - immediate: true, } - ); + store.resetWindowLevel(viewIdVal, imageIdVal); + }); watch(autoRange, (percentile) => { const image = imageData.value; diff --git a/src/config.ts b/src/config.ts index e98485bee..fb564eeee 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,6 +11,8 @@ import { Action } from './constants'; /** * These are the initial view IDs. + * + * These view IDs get mapped to components in core/viewTypes.ts. */ export const InitViewIDs: Record = { Coronal: 'Coronal', @@ -81,6 +83,20 @@ export const InitViewSpecs: Record = { props: { viewDirection: 'Posterior', viewUp: 'Superior', + slices: [ + { + viewID: InitViewIDs.ObliqueSagittal, + axis: 'Sagittal', + }, + { + viewID: InitViewIDs.ObliqueCoronal, + axis: 'Coronal', + }, + { + viewID: InitViewIDs.ObliqueAxial, + axis: 'Axial', + }, + ], }, }, }; diff --git a/src/constants.ts b/src/constants.ts index 20a0229a5..8212207b9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,9 +1,4 @@ -import { Maybe } from '@/src/types'; import type { RGBColor } from '@kitware/vtk.js/types'; -import vtkResliceCursorWidget, { - vtkResliceCursorViewWidget, -} from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget'; -import { ComputedRef, InjectionKey, Ref } from 'vue'; export const EPSILON = 10e-6; export const NOOP = () => {}; @@ -14,22 +9,6 @@ export const DarkTheme = 'kw-dark'; export const LightTheme = 'kw-light'; export const DefaultTheme = DarkTheme; -/** - * Retrieves the global ResliceCursorWidget instance. - */ -export const VTKResliceCursor: InjectionKey = - Symbol('VTKResliceCursor'); - -export const VTKResliceCursorViewWidget: InjectionKey< - ComputedRef -> = Symbol('VTKResliceCursorViewWidget'); - -/** - * Retrieves the parent tool HTML element. - */ -export const ToolContainer: InjectionKey>> = - Symbol('ToolContainer'); - export const Messages = { WebGLLost: { title: 'Viewer Error', @@ -154,7 +133,7 @@ export const WLPresetsCT = { }; export const OBLIQUE_OUTLINE_COLORS: Record = { - ObliqueAxial: [51, 255, 51], // Green + ObliqueAxial: [0, 128, 255], // Blue ObliqueSagittal: [255, 255, 0], // Yellow ObliqueCoronal: [255, 51, 51], // Red }; diff --git a/src/core/provider.ts b/src/core/provider.ts index c74e18393..8b64a53d8 100644 --- a/src/core/provider.ts +++ b/src/core/provider.ts @@ -1,5 +1,4 @@ import { DICOMIO } from '../io/dicom'; -import ProxyWrapper from './proxies'; import PaintTool from './tools/paint'; /** @@ -7,16 +6,13 @@ import PaintTool from './tools/paint'; */ export function CorePiniaProviderPlugin({ paint, - proxies, dicomIO, }: { paint?: PaintTool; - proxies?: ProxyWrapper; dicomIO?: DICOMIO; } = {}) { const dependencies = { $paint: paint ?? new PaintTool(), - $proxies: proxies, $dicomIO: dicomIO ?? new DICOMIO(), }; return () => dependencies; diff --git a/src/core/proxies.ts b/src/core/proxies.ts deleted file mode 100644 index 60e13cb84..000000000 --- a/src/core/proxies.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { vtkObject } from '@kitware/vtk.js/interfaces'; -import { VtkProxy } from '@kitware/vtk.js/macros'; -import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy'; -import vtkProxyManager from '@kitware/vtk.js/Proxy/Core/ProxyManager'; -import vtkSourceProxy from '@kitware/vtk.js/Proxy/Core/SourceProxy'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; - -// mapped in proxy.js -export enum ViewProxyType { - Volume = 'View3D', - Slice = 'View2D', - Oblique = 'Oblique', - Oblique3D = 'Oblique3D', -} - -/** - * Wrapper around the vtkProxyManager, since we don't need some of - * its complexities. - */ -export default class ProxyManagerWrapper { - private viewProxies: Map; - private dataProxies: Map>; - public readonly proxyManager: vtkProxyManager; - - constructor(proxyManager: vtkProxyManager) { - this.viewProxies = new Map(); - this.dataProxies = new Map(); - this.proxyManager = proxyManager; - } - - clearAll() { - const deleteProxy = (proxy: VtkProxy) => - this.proxyManager.deleteProxy(proxy); - - this.viewProxies.forEach(deleteProxy); - this.dataProxies.forEach(deleteProxy); - - this.viewProxies.clear(); - this.dataProxies.clear(); - } - - createView(id: string, type: ViewProxyType) { - if (this.viewProxies.has(id)) { - throw new Error(`Cannot create a view with the same ID "${id}"`); - } - - const proxy = this.proxyManager.createProxy('Views', type, { - name: type, - }); - - this.viewProxies.set(id, proxy); - return proxy; - } - - getView(id: string) { - return this.viewProxies.get(id); - } - - deleteView(id: string) { - const proxy = this.viewProxies.get(id); - if (proxy) { - this.proxyManager.deleteProxy(proxy); - this.viewProxies.delete(id); - } - } - - addData(id: string, data: T) { - if (this.dataProxies.has(id)) { - return; - } - const proxy = this.proxyManager.createProxy>( - 'Sources', - 'TrivialProducer' - ); - proxy.setInputData(data); - this.dataProxies.set(id, proxy); - } - - getData(id: string) { - return | null>(this.dataProxies.get(id) ?? null); - } - - updateData(id: string, data: T) { - const proxy = this.dataProxies.get(id); - if (!proxy) { - return; - } - proxy.setInputData(data); - } - - deleteData(id: string) { - const proxy = this.dataProxies.get(id); - this.dataProxies.delete(id); - if (proxy) this.proxyManager.deleteProxy(proxy); - else throw new Error('Did not find proxy for ID'); - } - - getDataRepresentationForView( - dataID: string, - viewID: string - ): T | null { - const dataProxy = this.dataProxies.get(dataID); - const viewProxy = this.viewProxies.get(viewID); - if (!dataProxy || !viewProxy) { - return null; - } - - return this.proxyManager.getRepresentation(dataProxy, viewProxy); - } -} diff --git a/src/core/viewTypes.ts b/src/core/viewTypes.ts index 2646b080c..f66d2f6cb 100644 --- a/src/core/viewTypes.ts +++ b/src/core/viewTypes.ts @@ -1,12 +1,12 @@ -import VtkObliqueThreeView from '@/src/components/VtkObliqueThreeView.vue'; -import VtkObliqueView from '@/src/components/VtkObliqueView.vue'; -import VtkThreeView from '@/src/components/VtkThreeView.vue'; -import VtkTwoView from '@/src/components/VtkTwoView.vue'; import { Component } from 'vue'; +import MultiObliqueSliceViewer from '@/src/components/MultiObliqueSliceViewer.vue'; +import ObliqueSliceViewer from '@/src/components/ObliqueSliceViewer.vue'; +import SliceViewer from '@/src/components/SliceViewer.vue'; +import VolumeViewer from '@/src/components/VolumeViewer.vue'; export const ViewTypeToComponent: Record = { - '2D': VtkTwoView, - '3D': VtkThreeView, - Oblique: VtkObliqueView, - Oblique3D: VtkObliqueThreeView, + '2D': SliceViewer, + '3D': VolumeViewer, + Oblique: ObliqueSliceViewer, + Oblique3D: MultiObliqueSliceViewer, }; diff --git a/src/core/vtk/onViewMounted.ts b/src/core/vtk/onViewMounted.ts new file mode 100644 index 000000000..e36431fb4 --- /dev/null +++ b/src/core/vtk/onViewMounted.ts @@ -0,0 +1,36 @@ +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; +import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow'; +import { whenever } from '@vueuse/core'; +import { computed, onUnmounted } from 'vue'; + +function isViewMounted(renderWindowView: vtkOpenGLRenderWindow) { + const container = vtkFieldRef(renderWindowView, 'container'); + return computed(() => !!container.value); +} + +export function onViewMounted( + renderWindowView: vtkOpenGLRenderWindow, + callback: () => void +) { + const isMounted = isViewMounted(renderWindowView); + whenever(isMounted, () => { + callback(); + }); +} + +export function onViewUnmounted( + renderWindowView: vtkOpenGLRenderWindow, + callback: () => void +) { + const isMounted = isViewMounted(renderWindowView); + whenever( + computed(() => !isMounted.value), + () => { + callback(); + } + ); + + onUnmounted(() => { + callback(); + }); +} diff --git a/src/core/vtk/types.ts b/src/core/vtk/types.ts new file mode 100644 index 000000000..527029932 --- /dev/null +++ b/src/core/vtk/types.ts @@ -0,0 +1,42 @@ +import vtkAbstractMapper from '@kitware/vtk.js/Rendering/Core/AbstractMapper'; +import vtkProp from '@kitware/vtk.js/Rendering/Core/Prop'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow'; +import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; +import { vtkObject } from '@kitware/vtk.js/interfaces'; + +export type VtkObjectConstructor = { + newInstance(props?: any): T; +}; + +export interface RequestRenderOptions { + immediate?: boolean; +} + +export interface View { + renderWindow: vtkRenderWindow; + renderer: vtkRenderer; + interactor: vtkRenderWindowInteractor; + renderWindowView: vtkOpenGLRenderWindow; + widgetManager: vtkWidgetManager; + requestRender(opts?: RequestRenderOptions): void; +} + +export type vtkPropWithMapperProperty< + M extends vtkAbstractMapper = vtkAbstractMapper, + P extends vtkObject = vtkObject +> = vtkProp & { + setMapper(m: M): void; + getProperty(): P; +}; + +export interface Representation< + Actor extends vtkPropWithMapperProperty, + Mapper extends vtkAbstractMapper +> { + actor: Actor; + mapper: Mapper; + property: ReturnType; +} diff --git a/src/core/vtk/useMouseRangeManipulatorListener.ts b/src/core/vtk/useMouseRangeManipulatorListener.ts new file mode 100644 index 000000000..439fd1672 --- /dev/null +++ b/src/core/vtk/useMouseRangeManipulatorListener.ts @@ -0,0 +1,54 @@ +import { Maybe } from '@/src/types'; +import { watchCompare } from '@/src/utils/watchCompare'; +import vtkMouseRangeManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseRangeManipulator'; +import { capitalize } from '@kitware/vtk.js/macros'; +import { MaybeRef, ref, toRef, unref } from 'vue'; +import deepEqual from 'fast-deep-equal'; + +type ListenerType = 'vertical' | 'horizontal' | 'scroll'; + +const DEFAULT_STEP = 1; + +export function useMouseRangeManipulatorListener( + manipulator: MaybeRef>, + type: ListenerType, + range: MaybeRef>, + step: MaybeRef>, + initialValue?: number +) { + const internalValue = ref(initialValue ?? 0); + + watchCompare( + [toRef(range), toRef(step), toRef(manipulator)], + ([newRange, , manip], _, onCleanup) => { + if (!newRange || !manip) return; + + const setterName = `set${ + capitalize(type) as Capitalize + }Listener` as const; + const removerName = `remove${ + capitalize(type) as Capitalize + }Listener` as const; + + manip[setterName]( + newRange[0], + newRange[1], + unref(step) ?? DEFAULT_STEP, + () => internalValue.value, + (val) => { + internalValue.value = val; + } + ); + + onCleanup(() => { + manip[removerName](); + }); + }, + { + immediate: true, + compare: deepEqual, + } + ); + + return internalValue; +} diff --git a/src/core/vtk/useOrientationMarker.ts b/src/core/vtk/useOrientationMarker.ts new file mode 100644 index 000000000..aa76b8217 --- /dev/null +++ b/src/core/vtk/useOrientationMarker.ts @@ -0,0 +1,52 @@ +import { Maybe } from '@/src/types'; +import vtkOrientationMarkerWidget from '@kitware/vtk.js/Interaction/Widgets/OrientationMarkerWidget'; +import { Corners } from '@kitware/vtk.js/Interaction/Widgets/OrientationMarkerWidget/Constants'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import { MaybeRef, computed, onScopeDispose, unref, watchEffect } from 'vue'; + +export const DEFAULT_CORNER = Corners.BOTTOM_LEFT; +export const DEFAULT_VIEWPORT_SIZE = 0.1; + +export interface UseOrientationMarkerOptions { + corner?: Corners; + size?: number; +} + +export function useOrientationMarker( + actor: MaybeRef>, + interactor: vtkRenderWindowInteractor, + options?: MaybeRef> +) { + const widget = vtkOrientationMarkerWidget.newInstance({ + interactor, + }); + + watchEffect(() => { + const actorVal = unref(actor); + if (actorVal) { + widget.setActor(actorVal); + widget.setEnabled(true); + } else { + widget.setEnabled(false); + } + }); + + onScopeDispose(() => { + widget.setEnabled(false); + }); + + const corner = computed(() => unref(options)?.corner ?? DEFAULT_CORNER); + watchEffect(() => { + widget.setViewportCorner(corner.value); + }); + + const viewportSize = computed( + () => unref(options)?.size ?? DEFAULT_VIEWPORT_SIZE + ); + watchEffect(() => { + widget.setViewportSize(viewportSize.value); + }); + + return { widget }; +} diff --git a/src/core/vtk/useResliceRepresentation.ts b/src/core/vtk/useResliceRepresentation.ts new file mode 100644 index 000000000..b755ea89e --- /dev/null +++ b/src/core/vtk/useResliceRepresentation.ts @@ -0,0 +1,28 @@ +import { MaybeRef } from 'vue'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import { useVtkRepresentation } from '@/src/core/vtk/useVtkRepresentation'; +import { Maybe } from '@/src/types'; +import { View } from '@/src/core/vtk/types'; +import vtkImageResliceMapper from '@kitware/vtk.js/Rendering/Core/ImageResliceMapper'; +import { onVTKEvent } from '@/src/composables/onVTKEvent'; +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; + +export function useResliceRepresentation( + view: View, + imageData: MaybeRef> +) { + const sliceRep = useVtkRepresentation({ + view, + data: imageData, + vtkActorClass: vtkImageSlice, + vtkMapperClass: vtkImageResliceMapper, + }); + + const plane = vtkFieldRef(sliceRep.mapper, 'slicePlane'); + onVTKEvent(plane, 'onModified', () => { + view.requestRender(); + }); + + return sliceRep; +} diff --git a/src/core/vtk/useSliceRepresentation.ts b/src/core/vtk/useSliceRepresentation.ts new file mode 100644 index 000000000..2ac5cc5a1 --- /dev/null +++ b/src/core/vtk/useSliceRepresentation.ts @@ -0,0 +1,21 @@ +import { MaybeRef } from 'vue'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper'; +import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import { useVtkRepresentation } from '@/src/core/vtk/useVtkRepresentation'; +import { Maybe } from '@/src/types'; +import { View } from '@/src/core/vtk/types'; + +export function useSliceRepresentation( + view: View, + imageData: MaybeRef> +) { + const sliceRep = useVtkRepresentation({ + view, + data: imageData, + vtkActorClass: vtkImageSlice, + vtkMapperClass: vtkImageMapper, + }); + + return sliceRep; +} diff --git a/src/core/vtk/useVolumeRepresentation.ts b/src/core/vtk/useVolumeRepresentation.ts new file mode 100644 index 000000000..2996aaa67 --- /dev/null +++ b/src/core/vtk/useVolumeRepresentation.ts @@ -0,0 +1,21 @@ +import { MaybeRef } from 'vue'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import { useVtkRepresentation } from '@/src/core/vtk/useVtkRepresentation'; +import { Maybe } from '@/src/types'; +import { View } from '@/src/core/vtk/types'; +import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; + +export function useVolumeRepresentation( + view: View, + imageData: MaybeRef> +) { + const volRep = useVtkRepresentation({ + view, + data: imageData, + vtkActorClass: vtkVolume, + vtkMapperClass: vtkVolumeMapper, + }); + + return volRep; +} diff --git a/src/core/vtk/useVtkFilter.ts b/src/core/vtk/useVtkFilter.ts new file mode 100644 index 000000000..0c50fbe45 --- /dev/null +++ b/src/core/vtk/useVtkFilter.ts @@ -0,0 +1,41 @@ +import { VtkObjectConstructor } from '@/src/core/vtk/types'; +import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef'; +import { Maybe } from '@/src/types'; +import { vtkAlgorithm, vtkObject } from '@kitware/vtk.js/interfaces'; +import { computedWithControl } from '@vueuse/core'; +import { ComputedRef, MaybeRef, onScopeDispose, unref, watchEffect } from 'vue'; + +export function useVtkFilter( + filterClass: VtkObjectConstructor, + ...inputData: MaybeRef>[] +) { + const filter = filterClass.newInstance(); + const mtime = vtkFieldRef(filter as vtkObject, 'mTime'); + + watchEffect(() => { + inputData + .map((input) => unref(input)) + .forEach((input, port) => { + if (input) filter.setInputData(unref(input), port); + }); + }); + + let cache: Record> = {}; + + const getOutputData = (port = 0) => { + if (!(port in cache)) { + cache[port] = computedWithControl(mtime, () => { + if (!filter.getInputData(port)) return null; + return filter.getOutputData(port) as D; + }); + } + return cache[port] as ComputedRef; + }; + + onScopeDispose(() => { + cache = {}; + filter.delete(); + }); + + return { filter, getOutputData }; +} diff --git a/src/core/vtk/useVtkInteractionManipulator.ts b/src/core/vtk/useVtkInteractionManipulator.ts new file mode 100644 index 000000000..8ac93ddde --- /dev/null +++ b/src/core/vtk/useVtkInteractionManipulator.ts @@ -0,0 +1,59 @@ +import { MaybeRef, computed, ref, toRef, unref, watch, watchEffect } from 'vue'; +import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator'; +import { VtkObjectConstructor } from '@/src/core/vtk/types'; +import { FirstParam } from '@/src/types'; +import { stableDeepRef } from '@/src/composables/stableDeepRef'; + +function addManipulator(style: vtkInteractorStyleManipulator, manip: any) { + if (manip.isA('vtkCompositeMouseManipulator')) { + style.addMouseManipulator(manip); + } else if (manip.isA('vtkCompositeGestureManipulator')) { + style.addGestureManipulator(manip); + } else if (manip.isA('vtkCompositeKeyboardManipulator')) { + style.addKeyboardManipulator(manip); + } +} + +function removeManipulator(style: vtkInteractorStyleManipulator, manip: any) { + if (manip.isA('vtkCompositeMouseManipulator')) { + style.removeMouseManipulator(manip); + } else if (manip.isA('vtkCompositeGestureManipulator')) { + style.removeGestureManipulator(manip); + } else if (manip.isA('vtkCompositeKeyboardManipulator')) { + style.removeKeyboardManipulator(manip); + } +} + +export function useVtkInteractionManipulator< + T extends VtkObjectConstructor +>( + style: vtkInteractorStyleManipulator, + vtkCtor: MaybeRef, + props: MaybeRef> +) { + const stableProps = stableDeepRef(toRef(props)); + const manipulator = computed(() => { + return unref(vtkCtor).newInstance(stableProps.value); + }); + + const enabled = ref(true); + + watch(manipulator, (_, oldManipulator) => { + oldManipulator?.delete(); + }); + + watchEffect((onCleanup) => { + if (!enabled.value) return; + + const manip = manipulator.value; + addManipulator(style, manip); + onCleanup(() => { + if (!style.isDeleted()) removeManipulator(style, manip); + }); + }); + + return { + instance: manipulator, + enabled, + }; +} diff --git a/src/core/vtk/useVtkInteractorStyle.ts b/src/core/vtk/useVtkInteractorStyle.ts new file mode 100644 index 000000000..54254f616 --- /dev/null +++ b/src/core/vtk/useVtkInteractorStyle.ts @@ -0,0 +1,25 @@ +import { VtkObjectConstructor } from '@/src/core/vtk/types'; +import vtkInteractorStyle from '@kitware/vtk.js/Rendering/Core/InteractorStyle'; +import { vtkWarningMacro } from '@kitware/vtk.js/macros'; +import { onScopeDispose } from 'vue'; +import type { View } from '@/src/core/vtk/types'; + +export function useVtkInteractorStyle( + vtkCtor: VtkObjectConstructor, + view: View +) { + const style = vtkCtor.newInstance(); + + if (view.interactor.getInteractorStyle()) { + vtkWarningMacro('Overwriting an already set interactor style'); + } + view.interactor.setInteractorStyle(style); + + onScopeDispose(() => { + if (view.interactor.getInteractorStyle() === style) + view.interactor.setInteractorStyle(null); + style.delete(); + }); + + return { interactorStyle: style }; +} diff --git a/src/core/vtk/useVtkRepresentation.ts b/src/core/vtk/useVtkRepresentation.ts new file mode 100644 index 000000000..70310bf2c --- /dev/null +++ b/src/core/vtk/useVtkRepresentation.ts @@ -0,0 +1,61 @@ +import { MaybeRef, onScopeDispose, unref, watchEffect } from 'vue'; +import { vtkObject } from '@kitware/vtk.js/interfaces'; +import vtkAbstractMapper from '@kitware/vtk.js/Rendering/Core/AbstractMapper'; +import type { + View, + VtkObjectConstructor, + vtkPropWithMapperProperty, +} from '@/src/core/vtk/types'; +import { Maybe } from '@/src/types'; +import { onVTKEvent } from '@/src/composables/onVTKEvent'; + +export interface UseVtkRepresentationParameters { + view: View; + data: MaybeRef>; + vtkActorClass: VtkObjectConstructor; + vtkMapperClass: VtkObjectConstructor; +} + +export function useVtkRepresentation< + Actor extends vtkPropWithMapperProperty, + Mapper extends vtkAbstractMapper, + Property extends ReturnType +>({ + view, + data: dataObject, + vtkActorClass, + vtkMapperClass, +}: UseVtkRepresentationParameters) { + const actor = vtkActorClass.newInstance(); + const mapper = vtkMapperClass.newInstance(); + const property = actor.getProperty() as Property; + + actor.setMapper(mapper); + + watchEffect((onCleanup) => { + const data = unref(dataObject); + if (!data) return; + + const { renderer } = view; + mapper.setInputData(data, 0); + renderer.addActor(actor); + view.requestRender(); + + onCleanup(() => { + renderer.removeActor(actor); + }); + }); + + [actor, mapper, property].forEach((obj: vtkObject) => { + onVTKEvent(obj, 'onModified', () => { + view.requestRender(); + }); + }); + + onScopeDispose(() => { + actor.delete(); + mapper.delete(); + }); + + return { actor, mapper, property }; +} diff --git a/src/core/vtk/useVtkView.ts b/src/core/vtk/useVtkView.ts new file mode 100644 index 000000000..6a8f6f8df --- /dev/null +++ b/src/core/vtk/useVtkView.ts @@ -0,0 +1,147 @@ +import { onVTKEvent } from '@/src/composables/onVTKEvent'; +import { View } from '@/src/core/vtk/types'; +import { Maybe } from '@/src/types'; +import { batchForNextTask } from '@/src/utils/batchForNextTask'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow'; +import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; +import { useElementSize } from '@vueuse/core'; +import { + MaybeRef, + onScopeDispose, + unref, + watchEffect, + watchPostEffect, +} from 'vue'; + +export function useWebGLRenderWindow(container: MaybeRef>) { + const renderWindowView = vtkOpenGLRenderWindow.newInstance(); + + watchPostEffect((onCleanup) => { + const el = unref(container); + if (!el) return; + + renderWindowView.setContainer(el); + onCleanup(() => { + renderWindowView.setContainer(null as unknown as HTMLElement); + }); + }); + + onScopeDispose(() => { + renderWindowView.delete(); + }); + + return renderWindowView; +} + +export function useWidgetManager(renderer: vtkRenderer) { + const manager = vtkWidgetManager.newInstance(); + manager.setRenderer(renderer); + + const updatePickingState = () => { + const enabled = manager.getPickingEnabled(); + const widgetCount = manager.getWidgets().length; + if (!enabled && widgetCount) { + manager.enablePicking(); + } else if (enabled && !widgetCount) { + manager.disablePicking(); + } + }; + + onVTKEvent(manager, 'onModified', updatePickingState); + updatePickingState(); + + return manager; +} + +export function useVtkView(container: MaybeRef>): View { + const renderer = vtkRenderer.newInstance(); + const renderWindow = vtkRenderWindow.newInstance(); + renderWindow.addRenderer(renderer); + + // the render window view + const renderWindowView = useWebGLRenderWindow(container); + renderWindow.addView(renderWindowView); + + onScopeDispose(() => { + renderWindow.removeView(renderWindowView); + }); + + // interactor + const interactor = vtkRenderWindowInteractor.newInstance(); + renderWindow.setInteractor(interactor); + interactor.setView(renderWindowView); + + watchPostEffect((onCleanup) => { + const el = unref(container); + if (!el) return; + + interactor.initialize(); + interactor.bindEvents(el); + onCleanup(() => { + if (interactor.getContainer()) interactor.unbindEvents(); + }); + }); + + // widget manager + const widgetManager = useWidgetManager(renderer); + + // render API + const deferredRender = batchForNextTask(() => { + // don't need to re-render during animation + if (interactor.isAnimating()) return; + widgetManager.renderWidgets(); + renderWindow.render(); + }); + + const immediateRender = () => { + if (interactor.isAnimating()) return; + renderWindow.render(); + }; + + const requestRender = ({ immediate } = { immediate: false }) => { + if (immediate) { + immediateRender(); + } + deferredRender(); + }; + + onVTKEvent(renderer, 'onModified', () => { + requestRender(); + }); + + // set size + const setSize = (width: number, height: number) => { + // ensure we have a non-zero size, otherwise + // the framebuffers might not be populated correctly + const scaledWidth = Math.max(1, width * globalThis.devicePixelRatio); + const scaledHeight = Math.max(1, height * globalThis.devicePixelRatio); + renderWindowView.setSize(scaledWidth, scaledHeight); + requestRender({ immediate: true }); + }; + + const { width, height } = useElementSize(container); + watchEffect(() => { + setSize(width.value, height.value); + }); + + // cleanup + onScopeDispose(() => { + renderWindow.removeRenderer(renderer); + + renderer.delete(); + renderWindow.delete(); + interactor.delete(); + }); + + return { + renderer, + renderWindow, + interactor, + renderWindowView, + widgetManager, + requestRender, + }; +} diff --git a/src/core/vtk/vtkFieldRef.ts b/src/core/vtk/vtkFieldRef.ts new file mode 100644 index 000000000..0597e02fe --- /dev/null +++ b/src/core/vtk/vtkFieldRef.ts @@ -0,0 +1,138 @@ +import { MaybeRef, Ref, computed, customRef, triggerRef, unref } from 'vue'; +import { vtkObject } from '@kitware/vtk.js/interfaces'; +import { capitalize } from '@kitware/vtk.js/macros'; +import { onPausableVTKEvent } from '@/src/composables/onPausableVTKEvent'; +import { batchForNextTask } from '@/src/utils/batchForNextTask'; +import { Maybe } from '@/src/types'; + +type NonEmptyString = T extends '' ? never : T; + +type FilterGetters = T extends `get${infer R}` + ? NonEmptyString> + : never; + +type GettableFields = FilterGetters; + +type NameToGetter = `get${Capitalize}`; + +type Just = Exclude; + +type GetterReturnType = T extends null | undefined + ? undefined + : NameToGetter extends keyof T + ? T[NameToGetter] extends (...args: any[]) => infer R + ? R + : never + : never; + +type ArraySetter = (...args: any[]) => boolean; + +export type GetterSetterFactory = { + get(): T; + set(v: T): boolean | undefined; +}; + +/** + * A custom set/get vtk object ref that operates based on the given field name. + * @param obj + * @param fieldName + */ +export function vtkFieldRef< + T extends Maybe, + F extends GettableFields> +>(obj: MaybeRef, fieldName: F): Ref>; + +/** + * A customRef wrapper that triggers the ref based on a vtk object modification event. + * @param obj + * @param factory + */ +export function vtkFieldRef, R>( + obj: MaybeRef, + factory: GetterSetterFactory +): Ref; + +export function vtkFieldRef>( + obj: MaybeRef, + fieldNameOrFactory: string | GetterSetterFactory +): any { + let getter: () => any; + let setter: (v: any) => boolean | undefined; + + if (typeof fieldNameOrFactory === 'string') { + const getterName = `get${capitalize(fieldNameOrFactory)}` as keyof T; + const setterName = `set${capitalize(fieldNameOrFactory)}` as keyof T; + + const _getter = computed( + () => unref(obj)?.[getterName] as (() => any) | undefined + ); + const _setter = computed( + () => + unref(obj)?.[setterName] as ((...args: any[]) => boolean) | undefined + ); + + const notNull = computed(() => !!unref(obj)); + const hasSetter = computed(() => { + const val = unref(obj); + return val ? setterName in val : false; + }); + + getter = () => _getter.value?.(); + setter = (v: any) => { + const set = _setter.value; + if (!notNull.value) return false; + if (!hasSetter.value || !set) + throw new Error(`No setter for field '${fieldNameOrFactory}'`); + + // handle certain array setters not accepting an array as input + if (Array.isArray(v) && set.length === v.length) { + return (set as ArraySetter)(...v); + } + return set(v); + }; + } else { + getter = fieldNameOrFactory.get; + setter = fieldNameOrFactory.set; + } + + let pause: () => void; + let resume: () => void; + + const ref = customRef((track, trigger) => { + return { + get: () => { + track(); + return getter(); + }, + set: (v) => { + let changed = false; + pause(); + + try { + const ret = setter(v); + // in the event a setter returns undefined, assume something changed. + changed = ret === undefined ? true : ret; + } finally { + resume(); + } + + if (changed) { + trigger(); + } + }, + }; + }); + + const onModified = batchForNextTask(() => { + if (unref(obj)?.isDeleted()) return; + triggerRef(ref); + }); + + ({ pause, resume } = onPausableVTKEvent( + obj as vtkObject, + 'onModified', + onModified + )); + + return ref; +} diff --git a/src/global.d.ts b/src/global.d.ts index 55a9bad5b..5739ed7e9 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,6 +1,5 @@ import 'pinia'; import type { Framework } from 'vuetify/types'; -import ProxyWrapper from './core/proxies'; import PaintTool from './core/tools/paint'; import { DICOMIO } from './io/dicom'; @@ -8,7 +7,6 @@ declare module 'pinia' { export interface PiniaCustomProperties { // from CorePiniaProviderPlugin $paint: PaintTool; - $proxies: ProxyWrapper; $dicomIO: DICOMIO; } } diff --git a/src/main.js b/src/main.js index 74ba7ec18..73d9c7175 100644 --- a/src/main.js +++ b/src/main.js @@ -9,7 +9,6 @@ import '@kitware/vtk.js/Rendering/OpenGL/Profiles/Glyph'; import { createApp } from 'vue'; import VueToast from 'vue-toastification'; import { createPinia } from 'pinia'; -import vtkProxyManager from '@kitware/vtk.js/Proxy/Core/ProxyManager'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import { setPipelinesBaseUrl, setPipelineWorkerUrl } from '@itk-wasm/image-io'; @@ -19,13 +18,10 @@ import vuetify from './plugins/vuetify'; import { DICOMIO } from './io/dicom'; import { FILE_READERS } from './io'; import { registerAllReaders } from './io/readers'; -import proxyConfiguration from './vtk/proxy'; import { CorePiniaProviderPlugin } from './core/provider'; -import ProxyManagerWrapper from './core/proxies'; import { patchExitPointerLock } from './utils/hacks'; import { init as initErrorReporting } from './utils/errorReporting'; import { StoreRegistry } from './plugins/storeRegistry'; -import { PROXY_MANAGER_WRAPPER_INJECT_KEY } from './composables/useProxyManager'; // patches patchExitPointerLock(); @@ -38,10 +34,6 @@ vtkMapper.setResolveCoincidentTopologyLineOffsetParameters(-3, -3); registerAllReaders(FILE_READERS); -const proxyManagerWrapper = new ProxyManagerWrapper( - vtkProxyManager.newInstance({ proxyConfiguration }) -); - const dicomIO = new DICOMIO(); dicomIO.initialize(); @@ -52,7 +44,6 @@ setPipelinesBaseUrl(itkConfig.imageIOUrl); const pinia = createPinia(); pinia.use( CorePiniaProviderPlugin({ - proxies: proxyManagerWrapper, dicomIO, }) ); @@ -62,7 +53,6 @@ const app = createApp(App); initErrorReporting(app); -app.provide(PROXY_MANAGER_WRAPPER_INJECT_KEY, proxyManagerWrapper); app.use(pinia); app.use(VueToast); app.use(vuetify); diff --git a/src/shims-vtk.d.ts b/src/shims-vtk.d.ts index f1ee4b343..37bed7f20 100644 --- a/src/shims-vtk.d.ts +++ b/src/shims-vtk.d.ts @@ -271,8 +271,40 @@ declare module '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget' { getCenter(): Vector3; setScrollingMethod(mode: number): boolean; setOpacity(opacity: number): boolean; + setImage(image: vtkImageData): boolean; + getImage(): vtkImageData; } // Just forwarding vtk-js's definition as default export: export default vtkResliceCursorWidget; } + +declare module '@kitware/vtk.js/Interaction/Widgets/PiecewiseGaussianWidget' { + import type { Vector2 } from '@kitware/vtk.js/types'; + import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; + + export interface vtkPiecewiseGaussianWidget {} + + function applyGaussianToPiecewiseFunction( + gaussians: any, + sampling: number, + rangeToUse: Vector2, + piecewiseFunction: vtkPiecewiseFunction + ): void; + + export declare const vtkPiecewiseGaussianWidget: { + applyGaussianToPiecewiseFunction: typeof applyGaussianToPiecewiseFunction; + }; + + export default vtkPiecewiseWidget; +} + +declare module '@kitware/vtk.js/Filters/Core/Cutter' { + export type vtkCutter = any; + export declare const vtkCutter: any; + export default vtkCutter; +} + +declare module '@kitware/vtk.js/Rendering/Core/AnnotatedCubeActor/Presets' { + export default {} as any; +} diff --git a/src/store/datasets-images.ts b/src/store/datasets-images.ts index d40d336d7..5d18ea739 100644 --- a/src/store/datasets-images.ts +++ b/src/store/datasets-images.ts @@ -42,8 +42,6 @@ export const useImageStore = defineStore('images', { this.idList.push(id); this.dataIndex[id] = imageData; - this.$proxies.addData(id, imageData); - this.metadata[id] = { ...defaultImageMetadata(), name }; this.updateData(id, imageData); return id; @@ -65,10 +63,7 @@ export const useImageStore = defineStore('images', { this.metadata[id] = metadata; this.dataIndex[id] = imageData; - - this.$proxies.updateData(id, imageData); } - this.$proxies.updateData(id, imageData); this.dataIndex[id] = imageData; }, diff --git a/src/store/datasets-layers.ts b/src/store/datasets-layers.ts index a76256294..54442a5d2 100644 --- a/src/store/datasets-layers.ts +++ b/src/store/datasets-layers.ts @@ -102,9 +102,6 @@ export const useLayersStore = defineStore('layer', () => { image = vtkITKHelper.convertItkToVtkImage(itkImage); } - this.$proxies.addData(id, image); - - // calling after adding data to proxy manager delays enabling deletion, thus avoiding error this.layerImages[id] = image; } @@ -149,10 +146,6 @@ export const useLayersStore = defineStore('layer', () => { ); delete this.layerImages[layerToDelete.id]; - - // May have errored creating data, so check before delete - if (this.$proxies.getData(layerToDelete.id)) - this.$proxies.deleteData(layerToDelete.id); } function getLayers(key: DataSelection | null | undefined) { diff --git a/src/store/datasets-models.ts b/src/store/datasets-models.ts index b764b46e2..1a32a1d0e 100644 --- a/src/store/datasets-models.ts +++ b/src/store/datasets-models.ts @@ -18,7 +18,6 @@ export const useModelStore = defineStore('models', { const id = useIdStore().nextId(); this.idList.push(id); this.dataIndex[id] = polyData; - this.$proxies.addData(id, polyData); return id; }, }, diff --git a/src/store/datasets.ts b/src/store/datasets.ts index 1eec010a6..355bd7e17 100644 --- a/src/store/datasets.ts +++ b/src/store/datasets.ts @@ -1,5 +1,4 @@ import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; -import { vtkObject } from '@kitware/vtk.js/interfaces'; import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import { useDICOMStore } from './datasets-dicom'; @@ -89,8 +88,6 @@ export function selectionEquals(s1: DataSelection, s2: DataSelection) { } export const useDatasetStore = defineStore('dataset', () => { - type _This = ReturnType; - const imageStore = useImageStore(); const modelStore = useModelStore(); const dicomStore = useDICOMStore(); @@ -116,10 +113,6 @@ export const useDatasetStore = defineStore('dataset', () => { return [...imageStore.idList, ...modelStore.idList]; }); - function getDataProxyByID(this: _This, id: string) { - return this.$proxies.getData(id); - } - // --- actions --- // function setPrimarySelection(sel: DataSelection | null) { @@ -192,7 +185,6 @@ export const useDatasetStore = defineStore('dataset', () => { primarySelection, primaryDataset, allDataIDs, - getDataProxyByID, setPrimarySelection, serialize, }; diff --git a/src/store/reslice-cursor.ts b/src/store/reslice-cursor.ts new file mode 100644 index 000000000..d03b716c8 --- /dev/null +++ b/src/store/reslice-cursor.ts @@ -0,0 +1,85 @@ +import { useCurrentImage } from '@/src/composables/useCurrentImage'; +import { ImageMetadata } from '@/src/types/image'; +import { LPSAxis } from '@/src/types/lps'; +import { ViewTypes } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants'; +import vtkResliceCursorWidget, { + ResliceCursorWidgetState, +} from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget'; +import type { Vector3 } from '@kitware/vtk.js/types'; +import { defineStore } from 'pinia'; +import { toRaw, watchEffect } from 'vue'; + +export function mapAxisToViewType(axis: LPSAxis) { + switch (axis) { + case 'Coronal': + return ViewTypes.XZ_PLANE; + case 'Sagittal': + return ViewTypes.YZ_PLANE; + case 'Axial': + return ViewTypes.XY_PLANE; + default: + throw new Error(`Invalid view axis: ${axis}`); + } +} + +function resetReslicePlanes( + resliceCursorState: ResliceCursorWidgetState, + imageMetadata: ImageMetadata +) { + const { Inferior, Anterior, Superior, Left } = toRaw( + imageMetadata.lpsOrientation + ); + const planes = { + [ViewTypes.YZ_PLANE]: { + normal: Left as Vector3, + viewUp: Superior as Vector3, + }, + [ViewTypes.XZ_PLANE]: { + normal: Anterior as Vector3, + viewUp: Superior as Vector3, + }, + [ViewTypes.XY_PLANE]: { + normal: Inferior as Vector3, + viewUp: Anterior as Vector3, + }, + }; + + resliceCursorState.setPlanes(planes); +} + +function useResliceInit( + resliceCursor: vtkResliceCursorWidget, + resliceCursorState: ResliceCursorWidgetState +) { + const { currentImageData, currentImageMetadata } = useCurrentImage(); + + watchEffect(() => { + const image = currentImageData.value; + if (!image) return; + resliceCursor.setImage(image); + // Reset to default plane values before transforming based on current image-data. + resetReslicePlanes(resliceCursorState, currentImageMetadata.value); + }); +} + +const useResliceCursorStore = defineStore('resliceCursor', () => { + const resliceCursor = vtkResliceCursorWidget.newInstance({ + scaleInPixels: true, + rotationHandlePosition: 0.75, + }) as vtkResliceCursorWidget; + + const widgetState = + resliceCursor.getWidgetState() as ResliceCursorWidgetState; + + useResliceInit(resliceCursor, widgetState); + + return { + resliceCursor, + resliceCursorState: widgetState, + resetReslicePlanes: (imageMetadata: ImageMetadata) => { + resetReslicePlanes(widgetState, imageMetadata); + }, + }; +}); + +export default useResliceCursorStore; diff --git a/src/store/segmentGroups.ts b/src/store/segmentGroups.ts index 97ef0645a..e2602545c 100644 --- a/src/store/segmentGroups.ts +++ b/src/store/segmentGroups.ts @@ -1,7 +1,6 @@ import { computed, reactive, ref, toRaw, watch } from 'vue'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; -import { RGBAColor } from '@kitware/vtk.js/types'; import { defineStore } from 'pinia'; import { useImageStore } from '@/src/store/datasets-images'; import { join, normalize } from '@/src/utils/path'; @@ -12,6 +11,7 @@ import { compareImageSpaces } from '@/src/utils/imageSpace'; import { SegmentMask } from '@/src/types/segment'; import { DEFAULT_SEGMENT_MASKS } from '@/src/config'; import { readImage, writeImage } from '@/src/io/readWriteImage'; +import type { RGBAColor } from '@kitware/vtk.js/types'; import vtkLabelMap from '../vtk/LabelMap'; import { StateFile, @@ -143,8 +143,6 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => { orderByParent.value[metadata.parentImage] ??= []; orderByParent.value[metadata.parentImage].push(id); - this.$proxies.addData(id, labelmap); - return id; } @@ -402,7 +400,6 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => { const id = useIdStore().nextId(); dataIndex[id] = labelmapImage; - this.$proxies.addData(id, labelmapImage); return id; }) ); diff --git a/src/store/view-animation.ts b/src/store/view-animation.ts new file mode 100644 index 000000000..4680b0779 --- /dev/null +++ b/src/store/view-animation.ts @@ -0,0 +1,75 @@ +import { defineStore } from 'pinia'; +import { ref, shallowRef, triggerRef, computed } from 'vue'; + +export interface ViewFilterSpec { + byViewType?: string[]; + byViewIds?: string[]; +} + +export function matchesViewFilter( + viewID: string, + viewType: string, + filterSpec?: ViewFilterSpec +) { + if (!filterSpec) return true; + const { byViewType, byViewIds } = filterSpec; + if (byViewType && !byViewType.includes(viewType)) return false; + if (byViewIds && !byViewIds.includes(viewID)) return false; + return true; +} + +function mergeArrayFilters(arr1?: string[], arr2?: string[]) { + if (!arr1 || !arr2) return undefined; + return Array.from(new Set(arr1.concat(arr2))); +} + +/** + * Merges view filter specs. + * + * An undefined filter spec means no filter. + * @param filterSpecs + * @returns + */ +export function mergeViewFilters( + filterSpecs: Array +) { + if (filterSpecs.length === 0) return undefined; + return filterSpecs.reduce((result, spec) => { + if (result === undefined) return undefined; + const byViewIds = mergeArrayFilters(result?.byViewIds, spec?.byViewIds); + const byViewType = mergeArrayFilters(result?.byViewType, spec?.byViewType); + return { byViewIds, byViewType }; + }, filterSpecs[0] as ViewFilterSpec | undefined); +} + +const useViewAnimationStore = defineStore('viewAnimation', () => { + const animating = ref(false); + const requestors = shallowRef(new Map()); + const viewFilter = computed(() => { + return mergeViewFilters(Array.from(requestors.value.values())); + }); + + const requestAnimation = (requestor: any, filter?: ViewFilterSpec) => { + if (requestors.value.has(requestor)) return; + requestors.value.set(requestor, filter); + animating.value = true; + triggerRef(requestors); + }; + + const cancelAnimation = (requestor: any) => { + requestors.value.delete(requestor); + if (requestors.value.size === 0) { + animating.value = false; + } + triggerRef(requestors); + }; + + return { + animating, + viewFilter, + requestAnimation, + cancelAnimation, + }; +}); + +export default useViewAnimationStore; diff --git a/src/store/view-configs/volume-coloring.ts b/src/store/view-configs/volume-coloring.ts index e020e7236..24a8e749b 100644 --- a/src/store/view-configs/volume-coloring.ts +++ b/src/store/view-configs/volume-coloring.ts @@ -25,6 +25,8 @@ import { useImageStore } from '../datasets-images'; export const DEFAULT_AMBIENT = 0.2; export const DEFAULT_DIFFUSE = 0.7; export const DEFAULT_SPECULAR = 0.3; +export const DEFAULT_EDGE_GRADIENT = 0.2; +export const DEFAULT_SAMPLING_DISTANCE = 0.2; function getPresetFromImageModality(imageID: string) { const dicomStore = useDICOMStore(); diff --git a/src/store/views.ts b/src/store/views.ts index 03fb81fb0..d4513e73a 100644 --- a/src/store/views.ts +++ b/src/store/views.ts @@ -1,8 +1,5 @@ -import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; import { defineStore } from 'pinia'; import { DefaultViewSpec, InitViewSpecs } from '../config'; -import { ViewProxyType } from '../core/proxies'; import { Layout, LayoutDirection } from '../types/layout'; import { useViewConfigStore } from './view-configs'; import { ViewSpec } from '../types/views'; @@ -29,31 +26,8 @@ export const useViewStore = defineStore('view', { viewIDs(state) { return Object.keys(state.viewSpecs); }, - getViewProxy() { - return (id: string) => { - return this.$proxies.getView(id); - }; - }, - getDataRepresentationForView() { - return ( - dataID: string, - viewID: string - ) => { - return ( - this.$proxies.getDataRepresentationForView(dataID, viewID) - ); - }; - }, }, actions: { - createOrGetViewProxy( - id: string, - type: ViewProxyType - ) { - return ( - this.$proxies.getView(id) ?? this.$proxies.createView(id, type) - ); - }, addView(id: string) { if (!(id in this.viewSpecs)) { this.viewSpecs[id] = structuredClone(DefaultViewSpec); @@ -62,7 +36,6 @@ export const useViewStore = defineStore('view', { removeView(id: string) { if (id in this.viewSpecs) { delete this.viewSpecs[id]; - this.$proxies.deleteView(id); } }, setLayout(layout: Layout) { diff --git a/src/types/index.ts b/src/types/index.ts index 500d1b7d6..9cf7311eb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -57,3 +57,15 @@ export type DeepPartial = T extends object [P in keyof T]?: DeepPartial; } : T; + +export type FirstParam = T extends (first: infer R, ...args: any[]) => any + ? R + : never; + +/** + * The props passed to components when used as part of the LayoutGrid. + */ +export interface LayoutViewProps { + id: string; + type: string; +} diff --git a/src/types/vtk-types.ts b/src/types/vtk-types.ts index 2d91bca40..7f6a27682 100644 --- a/src/types/vtk-types.ts +++ b/src/types/vtk-types.ts @@ -1,7 +1,7 @@ import { vtkAlgorithm, vtkObject } from '@kitware/vtk.js/interfaces'; import vtkDataSet from '@kitware/vtk.js/Common/DataModel/DataSet'; -import vtkLPSView2DProxy from '../vtk/LPSView2DProxy'; -import vtkLPSView3DProxy from '../vtk/LPSView3DProxy'; +import { View } from '@/src/core/vtk/types'; +import vtkInteractorStyle from '@kitware/vtk.js/Rendering/Core/InteractorStyle'; export interface vtkClass { newInstance: () => vtkObject; @@ -17,4 +17,7 @@ export interface vtkWriter extends vtkObject { write: (data: vtkDataSet) => any; } -export type vtkLPSViewProxy = vtkLPSView2DProxy | vtkLPSView3DProxy; +export interface VtkViewApi extends View { + interactorStyle?: vtkInteractorStyle; + resetCamera(): void; +} diff --git a/src/utils/camera.ts b/src/utils/camera.ts new file mode 100644 index 000000000..874950621 --- /dev/null +++ b/src/utils/camera.ts @@ -0,0 +1,117 @@ +import { View } from '@/src/core/vtk/types'; +import { ImageMetadata } from '@/src/types/image'; +import { LPSAxisDir } from '@/src/types/lps'; +import { getLPSAxisFromDir, getLPSDirections } from '@/src/utils/lps'; +import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; +import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera'; +import { Vector2, Vector3 } from '@kitware/vtk.js/types'; +import { vec3 } from 'gl-matrix'; + +/** + * Given an eye frame, return the dimension indices corresponding to the horizontal and vertical dimensions. + * @param lookAxis + * @param eyeUpAxis + * @returns + */ +function eyeFrameDimIndices(lookAxis: 0 | 1 | 2, eyeUpAxis: 0 | 1 | 2) { + if (lookAxis === 0 && eyeUpAxis === 1) return [2, 1] as Vector2; + if (lookAxis === 0 && eyeUpAxis === 2) return [1, 2] as Vector2; + if (lookAxis === 1 && eyeUpAxis === 0) return [2, 0] as Vector2; + if (lookAxis === 1 && eyeUpAxis === 2) return [0, 2] as Vector2; + if (lookAxis === 2 && eyeUpAxis === 0) return [1, 0] as Vector2; + if (lookAxis === 2 && eyeUpAxis === 1) return [0, 1] as Vector2; + throw new Error(`Invalid lookAxis and eyeUpAxis: ${lookAxis}, ${eyeUpAxis}`); +} + +function computeParallelScale( + lookAxis: 0 | 1 | 2, + viewUpAxis: 0 | 1 | 2, + dimensions: Vector3 | vec3, + viewSize: Vector2 +) { + const [widthIndex, heightIndex] = eyeFrameDimIndices(lookAxis, viewUpAxis); + const width = dimensions[widthIndex]; + const height = dimensions[heightIndex]; + const dimAspect = width / height; + + const [viewWidth, viewHeight] = viewSize; + const viewAspect = viewWidth / viewHeight; + + let scale = height / 2; + if (viewAspect < dimAspect) { + scale = width / 2 / viewAspect; + } + + return scale; +} + +export function resizeToFit( + view: View, + lookAxis: 0 | 1 | 2, + upAxis: 0 | 1 | 2, + dimensions: Vector3 | vec3 +) { + const camera = view.renderer.getActiveCamera(); + camera.setParallelScale( + computeParallelScale( + lookAxis, + upAxis, + dimensions, + view.renderWindowView.getSize() + ) + ); +} + +export function positionCamera( + camera: vtkCamera, + directionOfProjection: Vector3, + viewUp: Vector3, + focalPoint: Vector3 +) { + const position = vec3.clone(focalPoint) as Vector3; + vec3.sub(position, position, directionOfProjection); + camera.setFocalPoint(...focalPoint); + camera.setPosition(...position); + camera.setDirectionOfProjection(...directionOfProjection); + camera.setViewUp(...viewUp); +} + +export function resetCameraToImage( + view: View, + metadata: ImageMetadata, + viewDirection: LPSAxisDir, + viewUp: LPSAxisDir +) { + const { worldBounds, orientation } = metadata; + const lpsDirections = getLPSDirections(orientation); + + const center = vtkBoundingBox.getCenter(worldBounds); + const camera = view.renderer.getActiveCamera(); + + const directionOfProjection = lpsDirections[viewDirection] as Vector3; + const camerViewUp = lpsDirections[viewUp] as Vector3; + positionCamera(camera, directionOfProjection, camerViewUp, center); + + view.renderer.resetCamera(worldBounds); + view.requestRender(); +} +export function resizeToFitImage( + view: View, + metadata: ImageMetadata, + viewDirection: LPSAxisDir, + viewUp: LPSAxisDir +) { + const { lpsOrientation, dimensions, spacing } = metadata; + const viewDirAxis = getLPSAxisFromDir(viewDirection); + const viewUpAxis = getLPSAxisFromDir(viewUp); + const lookAxis = lpsOrientation[viewDirAxis]; + const upAxis = lpsOrientation[viewUpAxis]; + const dimsWithSpacing: Vector3 = [ + dimensions[0] * spacing[0], + dimensions[1] * spacing[1], + dimensions[2] * spacing[2], + ]; + + resizeToFit(view, lookAxis, upAxis, dimsWithSpacing); + view.requestRender(); +} diff --git a/src/utils/color.ts b/src/utils/color.ts index b4b7454f0..e71f644ea 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -1,4 +1,4 @@ -import { RGBAColor } from '@kitware/vtk.js/types'; +import type { RGBAColor } from '@kitware/vtk.js/types'; /** * Converts an RGBA tuple to a hex string with alpha. diff --git a/src/utils/guardedWritableRef.ts b/src/utils/guardedWritableRef.ts new file mode 100644 index 000000000..5384e67fd --- /dev/null +++ b/src/utils/guardedWritableRef.ts @@ -0,0 +1,16 @@ +import { Ref, computed } from 'vue'; + +export function guardedWritableRef( + obj: Ref, + accept: (incoming: T, current: T) => boolean +) { + return computed({ + get: () => obj.value, + set: (v) => { + if (accept(v, obj.value)) { + // eslint-disable-next-line no-param-reassign + obj.value = v; + } + }, + }); +} diff --git a/src/utils/volumeProperties.ts b/src/utils/volumeProperties.ts new file mode 100644 index 000000000..34aa92e48 --- /dev/null +++ b/src/utils/volumeProperties.ts @@ -0,0 +1,228 @@ +import { + DEFAULT_AMBIENT, + DEFAULT_DIFFUSE, + DEFAULT_SPECULAR, +} from '@/src/store/view-configs/volume-coloring'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import { getDiagonalLength } from '@kitware/vtk.js/Common/DataModel/BoundingBox'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; +import vtkVolumeProperty from '@kitware/vtk.js/Rendering/Core/VolumeProperty'; +import { Vector3 } from '@kitware/vtk.js/types'; +import { vec3 } from 'gl-matrix'; + +/** + * Sets the volume sampling distance. + * @param mapper + * @param distance A value betweeen 0 and 1. + * @param imageData + */ +export function setSamplingDistance( + mapper: vtkVolumeMapper, + distance: number, + imageData: vtkImageData +) { + const sampleDistance = + 0.7 * + Math.sqrt( + imageData + .getSpacing() + .map((v) => v * v) + .reduce((a, b) => a + b, 0) + ); + mapper.setSampleDistance(sampleDistance * 2 ** (distance * 3.0 - 1.5)); +} + +/** + * Sets the edge gradient. + * @param property + * @param edgeGradient A value between 0 and 1. + * @param dataArray + */ +export function setEdgeGradient( + property: vtkVolumeProperty, + edgeGradient: number, + dataArray: vtkDataArray +) { + const numberOfComponents = dataArray.getNumberOfComponents(); + for (let component = 0; component < numberOfComponents; component++) { + if (edgeGradient === 0) { + property.setUseGradientOpacity(component, false); + // eslint-disable-next-line no-continue + continue; + } + + property.setUseGradientOpacity(component, true); + + const range = dataArray.getRange(component); + const width = range[1] - range[0]; + const minV = Math.max(0.0, edgeGradient - 0.3) / 0.7; + const minGradOpacity = + minV > 0 ? Math.exp(Math.log(width * 0.2) * minV ** 2) : 0; + const maxGradOpacity = Math.exp(Math.log(width * edgeGradient ** 2)); + + property.setGradientOpacityMinimumValue(component, minGradOpacity); + property.setGradientOpacityMaximumValue(component, maxGradOpacity); + } +} + +export interface SetCinematicLightingParameters { + renderer: vtkRenderer; + enabled: boolean; + center: Vector3; + lightFollowsCamera: boolean; +} + +export function setCinematicLighting({ + renderer, + enabled, + center, + lightFollowsCamera, +}: SetCinematicLightingParameters) { + if (renderer.getLights().length === 0) { + renderer.createLight(); + } + const light = renderer.getLights()[0]; + if (enabled) { + light.setFocalPoint(...center); + light.setColor(1, 1, 1); + light.setIntensity(1); + light.setConeAngle(90); + light.setPositional(true); + renderer.setTwoSidedLighting(false); + if (lightFollowsCamera) { + light.setLightTypeToHeadLight(); + renderer.updateLightsGeometryToFollowCamera(); + } else { + light.setLightTypeToSceneLight(); + } + } else { + light.setPositional(false); + } +} + +export interface SetCinematicVolumeSamplingParameters { + enabled: boolean; + mapper: vtkVolumeMapper; + quality: number; + isAnimating: boolean; + image: vtkImageData; +} + +export function setCinematicVolumeSampling({ + enabled, + mapper, + quality, + isAnimating, + image, +}: SetCinematicVolumeSamplingParameters) { + if (isAnimating) { + mapper.setSampleDistance(0.75); + mapper.setMaximumSamplesPerRay(1000); + mapper.setGlobalIlluminationReach(0); + mapper.setComputeNormalFromOpacity(false); + } else { + const dims = image.getDimensions(); + const spacing = image.getSpacing(); + const spatialDiagonal = vec3.length( + vec3.fromValues( + dims[0] * spacing[0], + dims[1] * spacing[1], + dims[2] * spacing[2] + ) + ); + + // Use the average spacing for sampling by default + let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0; + // Adjust the volume sampling by the quality slider value + sampleDistance /= quality > 1 ? 0.5 * quality ** 2 : 1.0; + const samplesPerRay = spatialDiagonal / sampleDistance + 1; + mapper.setMaximumSamplesPerRay(samplesPerRay); + mapper.setSampleDistance(sampleDistance); + // Adjust the global illumination reach by volume quality slider + mapper.setGlobalIlluminationReach(enabled ? 0.25 * quality : 0); + mapper.setComputeNormalFromOpacity(!enabled && quality > 2); + } +} + +export interface SetCinematicVolumeShadingParameters { + enabled: boolean; + image: vtkImageData; + property: vtkVolumeProperty; + ambient: number; + diffuse: number; + specular: number; + component?: number; +} + +export function setCinematicVolumeShading({ + enabled, + image, + property, + ambient, + diffuse, + specular, + component = 0, +}: SetCinematicVolumeShadingParameters) { + property.setScalarOpacityUnitDistance( + 0, + (0.5 * getDiagonalLength(image.getBounds())) / + Math.max(...image.getDimensions()) + ); + + property.setShade(true); + property.setUseGradientOpacity(component, !enabled); + property.setGradientOpacityMinimumValue(component, 0.0); + const dataRange = image.getPointData().getScalars().getRange(); + property.setGradientOpacityMaximumValue( + component, + (dataRange[1] - dataRange[0]) * 0.01 + ); + property.setGradientOpacityMinimumOpacity(component, 0.0); + property.setGradientOpacityMaximumOpacity(component, 1.0); + + // do not toggle these parameters when animating + property.setAmbient(enabled ? ambient : DEFAULT_AMBIENT); + property.setDiffuse(enabled ? diffuse : DEFAULT_DIFFUSE); + property.setSpecular(enabled ? specular : DEFAULT_SPECULAR); +} + +export interface SetCinematicVolumeScatterParameters { + enabled: boolean; + mapper: vtkVolumeMapper; + blending: number; +} + +export function setCinematicVolumeScatter({ + enabled, + mapper, + blending, +}: SetCinematicVolumeScatterParameters) { + (window as any).am = mapper; + mapper.setVolumetricScatteringBlending(enabled ? blending : 0); +} + +export interface SetCinematicLocalAmbientOcclusionParameters { + enabled: boolean; + mapper: vtkVolumeMapper; + kernelSize: number; + kernelRadius: number; +} + +export function setCinematicLocalAmbientOcclusion({ + enabled, + mapper, + kernelSize, + kernelRadius, +}: SetCinematicLocalAmbientOcclusionParameters) { + if (enabled) { + mapper.setLocalAmbientOcclusion(true); + mapper.setLAOKernelSize(kernelSize); + mapper.setLAOKernelRadius(kernelRadius); + } else { + mapper.setLocalAmbientOcclusion(false); + mapper.setLAOKernelSize(0); + mapper.setLAOKernelRadius(0); + } +} diff --git a/src/utils/vtk-helpers.ts b/src/utils/vtk-helpers.ts index bb4d86b9c..7d9b9c17b 100644 --- a/src/utils/vtk-helpers.ts +++ b/src/utils/vtk-helpers.ts @@ -1,10 +1,14 @@ import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow'; import type { Vector2, Vector3 } from '@kitware/vtk.js/types'; +import type { vtkObject } from '@kitware/vtk.js/interfaces'; import { intersectDisplayWithPlane } from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator'; -import { OpacityFunction } from '../types/views'; +import { OpacityFunction } from '@/src/types/views'; +import vtkFieldData from '@kitware/vtk.js/Common/DataModel/DataSetAttributes/FieldData'; +import { Maybe } from '@/src/types'; export function computeWorldToDisplay( xyz: Vector3, @@ -134,7 +138,7 @@ export function getShiftedOpacityFromPreset( // but preset values of zero should not const shifted = y && y - shiftAlpha; const yVal = Math.max(Math.min(shifted, 1), 0); - return [(x - xmin) / width + shift, yVal]; + return [(x - xmin) / width + shift, yVal] as Vector2; }); } return null; @@ -220,3 +224,55 @@ export function getOpacityRangeFromPreset(presetName: string) { } return null; } + +/** + * Applies a set of points to a piecewise function. + * @param pwf + * @param points + * @param range + */ +export function applyPointsToPiecewiseFunction( + pwf: vtkPiecewiseFunction, + points: Vector2[], + range: Vector2 +) { + const width = range[1] - range[0]; + const rescaled = points.map(([x, y]) => [x * width + range[0], y]); + + pwf.removeAllPoints(); + rescaled.forEach(([x, y]) => pwf.addPoint(x, y)); +} + +/** + * Applies a set of nodes to a piecewise function. + * @param nodes + * @param range + * @param pwf + */ +export function applyNodesToPiecewiseFunction( + pwf: vtkPiecewiseFunction, + nodes: any[], + range: Vector2 +) { + const width = range[1] - range[0]; + const rescaled = nodes.map((n) => ({ ...n, x: n.x * width + range[0] })); + + pwf.setNodes(rescaled); +} + +/** + * Gets the data array given by the arrayName and arrayLocation. + * @param obj + * @param arrayName + * @param arrayLocation + * @returns + */ +export function getDataArray( + obj: vtkObject, + arrayName: string, + arrayLocation: 'pointData' | 'cellData' +) { + const field: Maybe = obj.getReferenceByName(arrayLocation); + const array = field?.getArrayByName(arrayName); + return array; +} diff --git a/src/utils/watchCompare.ts b/src/utils/watchCompare.ts new file mode 100644 index 000000000..6b521c268 --- /dev/null +++ b/src/utils/watchCompare.ts @@ -0,0 +1,75 @@ +import { + WatchCallback, + WatchOptions, + WatchSource, + WatchStopHandle, + watch, +} from 'vue'; + +export interface WatchCompareOptions + extends WatchOptions { + compare: (a: any, b: any) => boolean; +} + +// Internal Types from vue +export type MultiWatchSources = (WatchSource | object)[]; + +export type MapSources = { + [K in keyof T]: T[K] extends WatchSource ? V : never; +}; +export type MapOldSources = { + [K in keyof T]: T[K] extends WatchSource + ? Immediate extends true + ? V | undefined + : V + : never; +}; + +// overloads +export function watchCompare< + T extends Readonly[]>, + Immediate extends Readonly = false +>( + sources: [...T], + cb: WatchCallback, MapOldSources>, + options?: WatchCompareOptions +): WatchStopHandle; +export function watchCompare = false>( + source: WatchSource, + cb: WatchCallback, + options?: WatchCompareOptions +): WatchStopHandle; +export function watchCompare< + T extends object, + Immediate extends Readonly = false +>( + source: T, + cb: WatchCallback, + options?: WatchCompareOptions +): WatchStopHandle; + +export function watchCompare = false>( + sources: any, + callback: any, + options?: WatchCompareOptions +): WatchStopHandle { + return watch( + sources, + (newData, oldData, onCleanup) => { + let cleanupFn: (() => void) | null = null; + const innerOnCleanup = (fn: () => void) => { + cleanupFn = fn; + }; + const changed = options?.compare(newData, oldData); + + onCleanup(() => { + if (!changed) return; + cleanupFn?.(); + }); + + if (changed) return; + callback(newData, oldData, innerOnCleanup); + }, + options + ); +} diff --git a/src/vtk/IJKSliceRepresentationProxy/index.d.ts b/src/vtk/IJKSliceRepresentationProxy/index.d.ts deleted file mode 100644 index ffb83670c..000000000 --- a/src/vtk/IJKSliceRepresentationProxy/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import vtkSliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/SliceRepresentationProxy'; - -export interface vtkIJKSliceRepresentationProxy - extends vtkSliceRepresentationProxy { - setOpacity(opacity: number): boolean; - getOpacity(): number; -} - -export function extend(publicAPI: any, model: any): void; - -export declare const vtkIJKSliceRepresentationProxy: { - extend: typeof extend; -}; - -export default vtkIJKSliceRepresentationProxy; diff --git a/src/vtk/IJKSliceRepresentationProxy/index.js b/src/vtk/IJKSliceRepresentationProxy/index.js deleted file mode 100644 index abe598e52..000000000 --- a/src/vtk/IJKSliceRepresentationProxy/index.js +++ /dev/null @@ -1,56 +0,0 @@ -import macro from '@kitware/vtk.js/macro'; - -import vtkSliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/SliceRepresentationProxy'; - -const SLICE_MODE_MAP = { - X: 'I', - Y: 'J', - Z: 'K', - I: 'I', - J: 'J', - K: 'K', -}; - -function vtkIJKSliceRepresentationProxy(publicAPI, model) { - model.classHierarchy.push('vtkIJKSliceRepresentationProxy'); - - const superClass = { ...publicAPI }; - - // pretend XYZ slicing is actually just IJK. - publicAPI.setSlicingMode = (modeString) => { - return superClass.setSlicingMode(SLICE_MODE_MAP[modeString]); - }; -} - -// ---------------------------------------------------------------------------- -// Object factory -// ---------------------------------------------------------------------------- - -const DEFAULT_VALUES = {}; - -// ---------------------------------------------------------------------------- - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); - - // Object methods - vtkSliceRepresentationProxy.extend(publicAPI, model); - - macro.proxyPropertyMapping(publicAPI, model, { - opacity: { modelKey: 'property', property: 'opacity' }, - }); - - // Object specific methods - vtkIJKSliceRepresentationProxy(publicAPI, model); -} - -// ---------------------------------------------------------------------------- - -export const newInstance = macro.newInstance( - extend, - 'vtkIJKSliceRepresentationProxy' -); - -// ---------------------------------------------------------------------------- - -export default { newInstance, extend }; diff --git a/src/vtk/LPSView2DProxy/index.d.ts b/src/vtk/LPSView2DProxy/index.d.ts deleted file mode 100644 index 283df555d..000000000 --- a/src/vtk/LPSView2DProxy/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import { vtkView2DProxy } from '@kitware/vtk.js/Proxy/Core/View2DProxy'; -import { ViewProxyCustomizations } from '@/src/vtk/LPSView3DProxy'; -import { LPSViewProxyBase } from '@/src/vtk/LPSViewProxyBase'; - -// LPSViewProxyBase changes some types from vtkView2DProxy -export interface vtkLPSView2DProxy extends LPSViewProxyBase, vtkView2DProxy { - resizeToFit(lookAxis: Vector3, viewUpAxis: Vector3, worldDims: Vector3); - resetCamera(boundsToUse?: number[]); - /** - * @param mode One of IJKXYZ - */ - setSlicingMode(mode: string); -} - -export function extend(publicAPI: object, model: object): void; - -export declare const vtkLPSView2DProxy: { - extend: typeof extend; -}; - -export default vtkLPSView2DProxy; diff --git a/src/vtk/LPSView2DProxy/index.js b/src/vtk/LPSView2DProxy/index.js deleted file mode 100644 index a119806d0..000000000 --- a/src/vtk/LPSView2DProxy/index.js +++ /dev/null @@ -1,94 +0,0 @@ -import macro from '@kitware/vtk.js/macro'; -import vtkView2DProxy from '@kitware/vtk.js/Proxy/Core/View2DProxy'; -import { applyLPSViewProxyBase } from '@/src/vtk/LPSViewProxyBase'; - -function vtkLPSView2DProxy(publicAPI, model) { - model.classHierarchy.push('vtkLPSView2DProxy'); - const superClass = { ...publicAPI }; - - // override; we will set the manipulator ourselves - publicAPI.bindRepresentationToManipulator = () => {}; - - // override reset camera to /just/ reset the camera - publicAPI.resetCamera = (boundsToUse = null) => { - model.renderer.resetCamera(boundsToUse); - }; - - // override addRepresentation - publicAPI.addRepresentation = (rep) => { - if (!rep) { - return; - } - if (model.representations.indexOf(rep) === -1) { - model.representations.push(rep); - model.renderer.addViewProp(rep); - } - - if (rep.setSlicingMode && model.slicingMode) { - rep.setSlicingMode(model.slicingMode); - } - }; - - publicAPI.setSlicingMode = (mode) => { - model.axis = 'IJK'.indexOf(mode); - if (superClass.setSlicingMode(mode) && mode) { - let count = model.representations.length; - while (count--) { - const rep = model.representations[count]; - if (rep.setSlicingMode) { - rep.setSlicingMode(mode); - } - } - } - }; - - publicAPI.resizeToFit = (lookAxis, viewUpAxis, dims) => { - const [w, h] = model.renderWindow.getViews()[0].getSize(); - let bw; - let bh; - if (lookAxis === 0 && viewUpAxis === 1) { - [, bh, bw] = dims; - } else if (lookAxis === 0 && viewUpAxis === 2) { - [, bw, bh] = dims; - } else if (lookAxis === 1 && viewUpAxis === 0) { - [bh, , bw] = dims; - } else if (lookAxis === 1 && viewUpAxis === 2) { - [bw, , bh] = dims; - } else if (lookAxis === 2 && viewUpAxis === 0) { - [bh, bw] = dims; - } else if (lookAxis === 2 && viewUpAxis === 1) { - [bw, bh] = dims; - } - - const viewAspect = w / h; - const boundsAspect = bw / bh; - let scale = 0; - if (viewAspect >= boundsAspect) { - scale = bh / 2; - } else { - scale = bw / 2 / viewAspect; - } - - model.camera.setParallelScale(scale); - }; -} - -const DEFAULT_VALUES = { - slicingMode: null, // XYZIJK. Null means fallback to model.axis. -}; - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, initialValues, DEFAULT_VALUES); - - // slicing mode overrides axis - macro.setGet(publicAPI, model, ['slicingMode']); - - vtkView2DProxy.extend(publicAPI, model, initialValues); - applyLPSViewProxyBase(publicAPI, model); - - vtkLPSView2DProxy(publicAPI, model); -} - -export const newInstance = macro.newInstance(extend, 'vtkLPSView2DProxy'); - -export default { newInstance, extend }; diff --git a/src/vtk/LPSView3DProxy/index.d.ts b/src/vtk/LPSView3DProxy/index.d.ts deleted file mode 100644 index 3cf841a61..000000000 --- a/src/vtk/LPSView3DProxy/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import { LPSViewProxyBase } from '@/src/vtk/LPSViewProxyBase'; - -export interface vtkLPSView3DProxy extends LPSViewProxyBase, vtkViewProxy {} - -export function extend(publicAPI: object, model: object): void; - -export declare const vtkLPSView3DProxy: { - extend: typeof extend; -}; - -export default vtkLPSView3DProxy; diff --git a/src/vtk/LPSView3DProxy/index.js b/src/vtk/LPSView3DProxy/index.js deleted file mode 100644 index 666566cad..000000000 --- a/src/vtk/LPSView3DProxy/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import macro from '@kitware/vtk.js/macro'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import { applyLPSViewProxyBase } from '@/src/vtk/LPSViewProxyBase'; - -function vtkLPSView3DProxy(publicAPI, model) { - model.classHierarchy.push('vtkLPSView3DProxy'); -} - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, initialValues); - - vtkViewProxy.extend(publicAPI, model, initialValues); - applyLPSViewProxyBase(publicAPI, model); - - vtkLPSView3DProxy(publicAPI, model); -} - -export const newInstance = macro.newInstance(extend, 'vtkLPSView3DProxy'); - -export default { newInstance, extend }; diff --git a/src/vtk/LPSViewProxyBase/index.d.ts b/src/vtk/LPSViewProxyBase/index.d.ts deleted file mode 100644 index 61ddbde25..000000000 --- a/src/vtk/LPSViewProxyBase/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import vtkViewProxy from '@kitware/vtk.js/Proxy/Core/ViewProxy'; -import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator'; -import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; - -export interface LPSViewProxyBase { - removeAllRepresentations(): void; - updateCamera(directionOfProjection: vec3, viewUp: vec3, focalPoint: vec3); - getInteractorStyle2D(): vtkInteractorStyleManipulator; - getInteractorStyle3D(): vtkInteractorStyleManipulator; - getWidgetManager(): vtkWidgetManager; -} - -export function applyLPSViewProxyBase(publicAPI: object, model: object); diff --git a/src/vtk/LPSViewProxyBase/index.js b/src/vtk/LPSViewProxyBase/index.js deleted file mode 100644 index ca30e70db..000000000 --- a/src/vtk/LPSViewProxyBase/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import { batchForNextTask } from '@/src/utils/batchForNextTask'; -import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager'; -import { CaptureOn } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants'; - -export function applyLPSViewProxyBase(publicAPI, model) { - model._widgetManager = vtkWidgetManager.newInstance({ - pickingEnabled: true, - captureOn: CaptureOn.MOUSE_MOVE, - }); - model._widgetManager.setRenderer(model.renderer); - - publicAPI.getWidgetManager = () => model._widgetManager; - - // override resize to avoid flickering from rendering later - publicAPI.resize = () => { - if (model.container) { - const dims = model.container.getBoundingClientRect(); - if (dims.width === dims.height && dims.width === 0) { - return; - } - const devicePixelRatio = window.devicePixelRatio || 1; - const width = Math.max(10, Math.floor(devicePixelRatio * dims.width)); - const height = Math.max(10, Math.floor(devicePixelRatio * dims.height)); - model.renderWindow.getViews()[0].setSize(width, height); - publicAPI.invokeResize({ width, height }); - publicAPI.render(); - } - }; - - // override render to not reset camera and to not render during animation - publicAPI.render = () => { - if (model.interactor.isAnimating()) return; - model.orientationWidget.updateMarkerOrientation(); - model.renderWindow.render(); - }; - - // provide a renderLater impl that schedules for the next js task - publicAPI.renderLater = batchForNextTask(() => { - publicAPI.render(); - }); - - // add helper function - publicAPI.removeAllRepresentations = () => { - model.representations.forEach((rep) => model.renderer.removeViewProp(rep)); - model.representations.length = 0; - }; - - publicAPI.updateCamera = (directionOfProjection, viewUp, focalPoint) => { - const position = vec3.clone(focalPoint); - vec3.sub(position, position, directionOfProjection); - model.camera.setFocalPoint(...focalPoint); - model.camera.setPosition(...position); - model.camera.setDirectionOfProjection(...directionOfProjection); - model.camera.setViewUp(...viewUp); - }; - - const { setContainer } = publicAPI; - publicAPI.setContainer = (el) => { - const changed = model.container !== el; - setContainer(el); - // disable the built-in corner annotations - model.cornerAnnotation.setContainer(null); - // trigger onModified - if (changed) publicAPI.modified(); - }; - - const { resetCamera } = publicAPI; - publicAPI.resetCamera = (...args) => { - resetCamera(args); - model.renderer.updateLightsGeometryToFollowCamera(); - }; - - const { delete: delete_ } = publicAPI; - publicAPI.delete = () => { - delete_(); - model._widgetManager.delete(); - }; -} diff --git a/src/vtk/LabelMapSliceRepProxy/index.d.ts b/src/vtk/LabelMapSliceRepProxy/index.d.ts deleted file mode 100644 index 9e1f163ca..000000000 --- a/src/vtk/LabelMapSliceRepProxy/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import vtkIJKSliceRepresentationProxy from '../IJKSliceRepresentationProxy'; - -export interface vtkLabelMapSliceRepProxy - extends vtkIJKSliceRepresentationProxy {} - -export function extend(publicAPI: object, model: object): void; - -export declare const vtkLabelMapSliceRepProxy: { - extend: typeof extend; -}; - -export default vtkLabelMapSliceRepProxy; diff --git a/src/vtk/LabelMapSliceRepProxy/index.js b/src/vtk/LabelMapSliceRepProxy/index.js deleted file mode 100644 index 59a08c87f..000000000 --- a/src/vtk/LabelMapSliceRepProxy/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import macro from '@kitware/vtk.js/macro'; -import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; -import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; -import ImagePropertyConstants from '@kitware/vtk.js/Rendering/Core/ImageProperty/Constants'; - -import vtkIJKSliceRepresentationProxy from '../IJKSliceRepresentationProxy'; - -const { InterpolationType } = ImagePropertyConstants; - -// ---------------------------------------------------------------------------- -// vtkLabelMapSliceRepProxy methods -// ---------------------------------------------------------------------------- - -function vtkLabelMapSliceRepProxy(publicAPI, model) { - // Set our className - model.classHierarchy.push('vtkLabelMapSliceRepProxy'); - - let labelMapSub = null; - - // needed for vtk.js >= 23.0.0 - model.property.setUseLookupTableScalarRange(true); - model.property.setInterpolationType(InterpolationType.NEAREST); - model.mapper.setRelativeCoincidentTopologyPolygonOffsetParameters(-2, -2); - - let cachedSegments = null; - - function updateTransferFunctions(labelmap) { - const segments = labelmap.getSegments(); - if (segments === cachedSegments) { - return; - } - // Cache the colormap using ref equality. This will - // avoid updating the colormap unnecessarily. - cachedSegments = segments; - - const cfun = vtkColorTransferFunction.newInstance(); - const ofun = vtkPiecewiseFunction.newInstance(); - - let maxValue = 0; - - segments.forEach((segment) => { - const r = segment.color[0] || 0; - const g = segment.color[1] || 0; - const b = segment.color[2] || 0; - const a = segment.color[3] || 0; - cfun.addRGBPoint(segment.value, r / 255, g / 255, b / 255); - ofun.addPoint(segment.value, a / 255); - - if (segment.value > maxValue) { - maxValue = segment.value; - } - }); - - // add min/max values of the colormap range - cfun.addRGBPoint(0, 0, 0, 0); - ofun.addPoint(0, 0); - cfun.addRGBPoint(maxValue + 1, 0, 0, 0); - ofun.addPoint(maxValue + 1, 0); - - model.property.setRGBTransferFunction(cfun); - model.property.setScalarOpacity(ofun); - } - - function setInputData(labelmap) { - if (labelMapSub) { - labelMapSub.unsubscribe(); - labelMapSub = null; - } - - if (labelmap) { - labelMapSub = labelmap.onModified(() => - updateTransferFunctions(labelmap) - ); - updateTransferFunctions(labelmap); - } - } - - // override because we manage our own color/opacity functions - publicAPI.setColorBy = () => {}; - - publicAPI.delete = macro.chain(publicAPI.delete, () => { - if (labelMapSub) { - labelMapSub.unsubscribe(); - labelMapSub = null; - } - }); - - // Keep things updated - model.sourceDependencies.push({ setInputData }); -} - -// ---------------------------------------------------------------------------- -// Object factory -// ---------------------------------------------------------------------------- - -const DEFAULT_VALUES = {}; - -// ---------------------------------------------------------------------------- - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); - - // Object methods - vtkIJKSliceRepresentationProxy.extend(publicAPI, model); - - // Object specific methods - vtkLabelMapSliceRepProxy(publicAPI, model); -} - -// ---------------------------------------------------------------------------- - -export const newInstance = macro.newInstance( - extend, - 'vtkLabelMapSliceRepProxy' -); - -// ---------------------------------------------------------------------------- - -export default { newInstance, extend }; diff --git a/src/vtk/MultiSliceRepresentationProxy/index.d.ts b/src/vtk/MultiSliceRepresentationProxy/index.d.ts deleted file mode 100644 index 14f9e9cc6..000000000 --- a/src/vtk/MultiSliceRepresentationProxy/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import vtkGeometryRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy'; -import { RGBColor } from '@kitware/vtk.js/types'; - -export type OutlineProperties = { - lineWidth: number | undefined; - color: RGBColor | undefined; - opacity: number | undefined; -}; - -export interface vtkMultiSliceRepresentationProxy - extends vtkGeometryRepresentationProxy { - setDataOutlineProperties(props: OutlineProperties): void; - setSliceOutlineProperties(props: OutlineProperties[]): void; - setPlanes(planes: { origin: vec3; normal: vec3 }[]): void; - setWindowWidth(width: number): boolean; - setWindowLevel(level: number): boolean; -} - -export default vtkMultiSliceRepresentationProxy; diff --git a/src/vtk/MultiSliceRepresentationProxy/index.js b/src/vtk/MultiSliceRepresentationProxy/index.js deleted file mode 100644 index 81a40e414..000000000 --- a/src/vtk/MultiSliceRepresentationProxy/index.js +++ /dev/null @@ -1,106 +0,0 @@ -import macro from '@kitware/vtk.js/macro'; - -import vtkGeometryRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy'; -import vtkResliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/ResliceRepresentationProxy'; -import vtkImageDataOutlineFilter from '@kitware/vtk.js/Filters/General/ImageDataOutlineFilter'; - -function vtkMultiSliceRepresentationProxy(publicAPI, model) { - model.classHierarchy.push('vtkMultiSliceRepresentationProxy'); - - // setup image outline - model.outlineFilter = vtkImageDataOutlineFilter.newInstance(); - model.outlineFilter.setGenerateFaces(false); - model.outlineFilter.setGenerateLines(true); - - // clear previous dependencies (mapper) - model.sourceDependencies.length = 0; - model.sourceDependencies.push(model.outlineFilter); - publicAPI.getMapper().setInputConnection(model.outlineFilter.getOutputPort()); - - // array of slices - model.slices = [ - vtkResliceRepresentationProxy.newInstance(), - vtkResliceRepresentationProxy.newInstance(), - vtkResliceRepresentationProxy.newInstance(), - ]; - - model.slices.forEach((sliceRep) => { - // add all actors to the composite representation: - sliceRep.getActors().forEach((sliceRepActor) => { - model.actors.push(sliceRepActor); - }); - }); - - publicAPI.setDataOutlineProperties = (props) => { - if (props.lineWidth) { - publicAPI.setLineWidth(props.lineWidth); - } - - if (props.color) { - publicAPI.setColor(props.color); - } - - if (props.opacity) { - publicAPI.setOpacity(props.opacity); - } - }; - - publicAPI.setSliceOutlineProperties = (props) => { - if (props.length === model.slices.length) { - model.slices.forEach((rep, idx) => { - if (props[idx].lineWidth) { - rep.setOutlineLineWidth(props[idx].lineWidth); - } - if (props[idx].color) { - rep.setOutlineColor(props[idx].color); - } - rep.setOutlineVisibility(true); - }); - } - }; - - publicAPI.setPlanes = (planes) => { - if (planes.length === model.slices.length) { - for (let i = 0; i < planes.length; ++i) { - model.slices[i].getSlicePlane().setNormal(planes[i].normal); - model.slices[i].getSlicePlane().setOrigin(planes[i].origin); - } - } - }; - - const _setInput = publicAPI.setInput; - publicAPI.setInput = (source) => { - _setInput(source); - model.slices.forEach((sliceRep) => sliceRep.setInput(source)); - }; - - publicAPI.setWindowWidth = (width) => - model.slices.forEach((r) => r.setWindowWidth(width)); - publicAPI.setWindowLevel = (level) => - model.slices.forEach((r) => r.setWindowLevel(level)); -} - -// ---------------------------------------------------------------------------- -// Object factory -// ---------------------------------------------------------------------------- - -const DEFAULT_VALUES = {}; - -// ---------------------------------------------------------------------------- - -export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); - vtkGeometryRepresentationProxy.extend(publicAPI, model); - vtkMultiSliceRepresentationProxy(publicAPI, model); -} - -// ---------------------------------------------------------------------------- - -export const newInstance = macro.newInstance( - extend, - 'vtkMultiSliceRepresentationProxy' -); - -// ---------------------------------------------------------------------------- - -export default { newInstance, extend }; diff --git a/src/vtk/PolygonWidget/behavior.ts b/src/vtk/PolygonWidget/behavior.ts index ad61e3b0b..6d4c80f02 100644 --- a/src/vtk/PolygonWidget/behavior.ts +++ b/src/vtk/PolygonWidget/behavior.ts @@ -1,6 +1,6 @@ import { distance2BetweenPoints } from '@kitware/vtk.js/Common/Core/Math'; import macro from '@kitware/vtk.js/macros'; -import { Vector3 } from '@kitware/vtk.js/types'; +import type { Vector3 } from '@kitware/vtk.js/types'; import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; import { WidgetAction } from '@/src/vtk/ToolWidgetUtils/types'; diff --git a/src/vtk/PolygonWidget/index.d.ts b/src/vtk/PolygonWidget/index.d.ts index e91832340..0c793a841 100644 --- a/src/vtk/PolygonWidget/index.d.ts +++ b/src/vtk/PolygonWidget/index.d.ts @@ -30,7 +30,8 @@ export interface vtkPolygonViewWidget extends vtkAnnotationToolWidget { export interface IPolygonWidgetInitialValues extends IAnnotationToolWidgetInitialValues {} -export interface vtkPolygonWidget extends vtkAbstractWidgetFactory { +export interface vtkPolygonWidget + extends vtkAbstractWidgetFactory { getLength(): number; getWidgetState(): vtkPolygonWidgetState; } diff --git a/src/vtk/RulerWidget/index.d.ts b/src/vtk/RulerWidget/index.d.ts index 7ecd171f5..b2c68b756 100644 --- a/src/vtk/RulerWidget/index.d.ts +++ b/src/vtk/RulerWidget/index.d.ts @@ -36,7 +36,8 @@ export interface IRulerWidgetInitialValues isPlaced: boolean; } -export interface vtkRulerWidget extends vtkAbstractWidgetFactory { +export interface vtkRulerWidget + extends vtkAbstractWidgetFactory { getLength(): number; getWidgetState(): vtkRulerWidgetState; } diff --git a/src/vtk/ToolWidgetUtils/types.ts b/src/vtk/ToolWidgetUtils/types.ts index 6ff4b3e6b..bc44a16b6 100644 --- a/src/vtk/ToolWidgetUtils/types.ts +++ b/src/vtk/ToolWidgetUtils/types.ts @@ -5,7 +5,7 @@ import vtkAbstractWidget from '@kitware/vtk.js/Widgets/Core/AbstractWidget'; import vtkWidgetState from '@kitware/vtk.js/Widgets/Core/WidgetState'; import vtkPlaneManipulator from '@kitware/vtk.js/Widgets/Manipulators/PlaneManipulator'; import { vtkSubscription } from '@kitware/vtk.js/interfaces'; -import { Vector2, Vector3 } from '@kitware/vtk.js/types'; +import type { Vector2, Vector3 } from '@kitware/vtk.js/types'; export type WidgetAction = { name: string; diff --git a/src/vtk/proxy.js b/src/vtk/proxy.js deleted file mode 100644 index bf0a7d335..000000000 --- a/src/vtk/proxy.js +++ /dev/null @@ -1,78 +0,0 @@ -import vtkLookupTableProxy from '@kitware/vtk.js/Proxy/Core/LookupTableProxy'; -import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy'; - -import vtkSourceProxy from '@kitware/vtk.js/Proxy/Core/SourceProxy'; - -import vtkGeometryRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy'; -import vtkResliceRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/ResliceRepresentationProxy'; -import vtkVolumeRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/VolumeRepresentationProxy'; -import vtkMultiSliceRepresentationProxy from '@/src/vtk/MultiSliceRepresentationProxy'; - -import vtkLPSView3DProxy from '@/src/vtk/LPSView3DProxy'; -import vtkLPSView2DProxy from '@/src/vtk/LPSView2DProxy'; -import vtkIJKSliceRepresentationProxy from '@/src/vtk/IJKSliceRepresentationProxy'; -import vtkLabelMapSliceRepProxy from '@/src/vtk/LabelMapSliceRepProxy'; - -function createProxyDefinition( - classFactory, - ui = [], - links = [], - definitionOptions = {}, - props = {} -) { - return { - class: classFactory, - options: { links, ui, ...definitionOptions }, - props, - }; -} - -function createDefaultView(classFactory, options = [], props = {}) { - return createProxyDefinition(classFactory, [], [], options, props); -} - -// ---------------------------------------------------------------------------- - -export default { - definitions: { - Proxy: { - LookupTable: createProxyDefinition(vtkLookupTableProxy, [], [], { - presetName: 'Default (Cool to Warm)', - }), - PiecewiseFunction: createProxyDefinition(vtkPiecewiseFunctionProxy), - }, - Sources: { - TrivialProducer: createProxyDefinition(vtkSourceProxy), - }, - Representations: { - ImageReslice: createProxyDefinition(vtkResliceRepresentationProxy), - ImageSlice: createProxyDefinition(vtkIJKSliceRepresentationProxy), - LabelMapSlice: createProxyDefinition(vtkLabelMapSliceRepProxy), - Volume: createProxyDefinition(vtkVolumeRepresentationProxy), - Geometry: createProxyDefinition(vtkGeometryRepresentationProxy), - ImageMultiSlice: createProxyDefinition(vtkMultiSliceRepresentationProxy), - }, - Views: { - View3D: createDefaultView(vtkLPSView3DProxy), - View2D: createDefaultView(vtkLPSView2DProxy), - Oblique: createDefaultView(vtkLPSView2DProxy), - Oblique3D: createDefaultView(vtkLPSView3DProxy), - }, - }, - representations: { - View3D: { - vtkImageData: { name: 'Volume' }, - vtkPolyData: { name: 'Geometry' }, - }, - View2D: { - vtkImageData: { name: 'ImageSlice' }, - vtkLabelMap: { name: 'LabelMapSlice' }, - }, - Oblique: { - vtkImageData: { name: 'ImageReslice' }, - }, - Oblique3D: { - vtkImageData: { name: 'ImageMultiSlice' }, - }, - }, -}; diff --git a/src/vtk/proxyUtils.js b/src/vtk/proxyUtils.js deleted file mode 100644 index f85213115..000000000 --- a/src/vtk/proxyUtils.js +++ /dev/null @@ -1,16 +0,0 @@ -export function createOrGetView(proxyManager, viewType, name) { - const views = proxyManager.getViews(); - const view = views.find((v) => v.getName() === name); - if (view) { - return view; - } - - return proxyManager.createProxy('Views', viewType, { name }); -} - -export function createFourUpViews(proxyManager) { - createOrGetView(proxyManager, 'ViewX', 'X:1'); - createOrGetView(proxyManager, 'ViewY', 'Y:1'); - createOrGetView(proxyManager, 'ViewZ', 'Z:1'); - createOrGetView(proxyManager, 'View3D', '3D:1'); -} diff --git a/vite.config.ts b/vite.config.ts index 3da1114f7..92273f57e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -29,7 +29,7 @@ function resolveNodeModulePath(moduleName: string) { return modulePath; } -function resolvePath(...args) { +function resolvePath(...args: string[]) { return normalizePath(path.resolve(...args)); } @@ -164,11 +164,17 @@ export default defineConfig({ dest: 'itk/pipelines', }, { - src: 'src/io/itk-dicom/emscripten-build/**/dicom*', + src: resolvePath( + rootDir, + 'src/io/itk-dicom/emscripten-build/**/dicom*' + ), dest: 'itk/pipelines', }, { - src: 'src/io/resample/emscripten-build/**/resample*', + src: resolvePath( + rootDir, + 'src/io/resample/emscripten-build/**/resample*' + ), dest: 'itk/pipelines', }, ],