Skip to content

Commit

Permalink
feat(Oblique3D): add 3D view in Oblique layout
Browse files Browse the repository at this point in the history
  • Loading branch information
jadh4v committed Aug 30, 2023
1 parent 68a5ab8 commit b34d440
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 16 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.321.1",
"@kitware/vtk.js": "^28.6.0",
"@kitware/vtk.js": "^28.8.3",
"@netlify/edge-functions": "^2.0.0",
"@sentry/vue": "^7.54.0",
"@vueuse/core": "^8.5.0",
Expand Down
2 changes: 2 additions & 0 deletions src/components/LayoutGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Component, computed, defineComponent, PropType, toRefs } from 'vue';
import { storeToRefs } from 'pinia';
import VtkTwoView from './VtkTwoView.vue';
import VtkObliqueView from './VtkObliqueView.vue';
import VtkObliqueThreeView from './VtkObliqueThreeView.vue';
import VtkThreeView from './VtkThreeView.vue';
import { Layout, LayoutDirection } from '../types/layout';
import { useViewStore } from '../store/views';
Expand All @@ -32,6 +33,7 @@ const TYPE_TO_COMPONENT: Record<ViewType, Component> = {
'2D': VtkTwoView,
'3D': VtkThreeView,
'Oblique': VtkObliqueView,
'Oblique3D': VtkObliqueThreeView,
};
export default defineComponent({
Expand Down
236 changes: 236 additions & 0 deletions src/components/VtkObliqueThreeView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<template>
<div class="vtk-container-wrapper vtk-three-container">
<div class="vtk-container" :class="active ? 'active' : ''">
<div class="vtk-sub-container">
<div
class="vtk-view"
ref="vtkContainerRef"
data-testid="vtk-view vtk-three-view"
/>
</div>
<div class="overlay-no-events tool-layer">
<pan-tool :viewId="viewID" />
</div>
<view-overlay-grid class="overlay-no-events view-annotations">
<template v-slot:top-left>
<div class="annotation-cell">
<v-btn
class="pointer-events-all"
dark
icon
size="medium"
variant="text"
@click="resetCamera"
>
<v-icon size="medium" class="py-1">
mdi-camera-flip-outline
</v-icon>
<v-tooltip
location="right"
activator="parent"
transition="slide-x-transition"
>
Reset Camera
</v-tooltip>
</v-btn>
</div>
</template>
</view-overlay-grid>
</div>
</div>
</template>

<script lang="ts">
import {
computed,
defineComponent,
inject,
onBeforeUnmount,
onMounted,
PropType,
ref,
toRefs,
watch,
} from 'vue';
import { storeToRefs } from 'pinia';
import { vec3 } from 'gl-matrix';
import vtkImageDataOutlineFilter from '@kitware/vtk.js/Filters/General/ImageDataOutlineFilter';
import vtkGeometryRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy';
import ViewOverlayGrid from '@src/components/ViewOverlayGrid.vue';
import { useVTKCallback } from '@/src/composables/useVTKCallback';
import PanTool from './tools/PanTool.vue';
import { LPSAxisDir } from '../types/lps';
import { useViewProxy } from '../composables/useViewProxy';
import vtkLPSView3DProxy from '../vtk/LPSView3DProxy';
import vtkLPSView2DProxy from '../vtk/LPSView2DProxy';
import { ViewProxyType } from '../core/proxies';
import { useCurrentImage } from '../composables/useCurrentImage';
import { useCameraOrientation } from '../composables/useCameraOrientation';
import { InitViewIDs } from '../config';
import { useResizeObserver } from '../composables/useResizeObserver';
import { useCustomEvents } from '../store/custom-events';
import { /* ToolContainer, VTKTwoViewWidgetManager, */ VTKResliceCursor } from '../constants';
import { useSceneBuilder } from '../composables/useSceneBuilder';
export default defineComponent({
props: {
id: {
type: String,
required: true,
},
viewDirection: {
type: String as PropType<LPSAxisDir>,
required: true,
},
viewUp: {
type: String as PropType<LPSAxisDir>,
required: true,
},
},
components: {
ViewOverlayGrid,
PanTool,
},
setup(props) {
const { id: viewID, viewDirection, viewUp } = toRefs(props);
const vtkContainerRef = ref<HTMLElement>();
// --- computed vars --- //
const { currentImageData, currentImageID, currentImageMetadata } = useCurrentImage();
// --- view proxy setup --- //
const { viewProxy, setContainer: setViewProxyContainer } =
useViewProxy<vtkLPSView3DProxy>(viewID, ViewProxyType.Oblique3D);
const { baseImageRep } = useSceneBuilder<vtkGeometryRepresentationProxy>(viewID, {
baseImage: currentImageID
});
// Get a 2D Oblique view proxy to fetch the reslice representations.
// We don't create new reslice reps, we just re-use those already
// created by the 2D Oblique Views.
const oblique2DViewProxies = computed(() => {
const { viewProxy: obliqueAxialViewProxy } =
useViewProxy<vtkLPSView2DProxy>(InitViewIDs.ObliqueAxial, ViewProxyType.Oblique);
const { viewProxy: obliqueSagittalViewProxy } =
useViewProxy<vtkLPSView2DProxy>(InitViewIDs.ObliqueSagittal, ViewProxyType.Oblique);
const { viewProxy: obliqueCoronalViewProxy } =
useViewProxy<vtkLPSView2DProxy>(InitViewIDs.ObliqueCoronal, ViewProxyType.Oblique);
return [obliqueAxialViewProxy, obliqueSagittalViewProxy, obliqueCoronalViewProxy];
});
function addResliceProxiesToView(): void {
oblique2DViewProxies.value?.forEach((proxyRef) => {
const reps = proxyRef.value?.getRepresentations();
if (reps && reps.length > 0) {
viewProxy.value.addRepresentation(reps[0]);
}
});
}
onBeforeUnmount(() => {
setViewProxyContainer(null);
viewProxy.value.setContainer(null);
});
onMounted(() => {
viewProxy.value.setOrientationAxesVisibility(true);
viewProxy.value.setOrientationAxesType('cube');
viewProxy.value.setBackground([0, 0, 0, 0]);
setViewProxyContainer(vtkContainerRef.value);
addResliceProxiesToView();
});
const resliceCursorRef = inject(VTKResliceCursor);
if (!resliceCursorRef) {
throw Error('Cannot access global ResliceCursor instance.');
}
const onPlanesUpdated = useVTKCallback(
resliceCursorRef.value.getWidgetState().onModified
);
onPlanesUpdated(() => {
viewProxy.value.renderLater();
});
// --- camera setup --- //
const { cameraUpVec, cameraDirVec } = useCameraOrientation(
viewDirection,
viewUp,
currentImageMetadata
);
useResizeObserver(vtkContainerRef, () => viewProxy.value.resize());
const resetCamera = () => {
const bounds = currentImageMetadata.value.worldBounds;
const center = [
(bounds[0] + bounds[1]) / 2,
(bounds[2] + bounds[3]) / 2,
(bounds[4] + bounds[5]) / 2,
] as vec3;
viewProxy.value.updateCamera(
cameraDirVec.value,
cameraUpVec.value,
center
);
addResliceProxiesToView();
viewProxy.value.resetCamera();
viewProxy.value.renderLater();
};
// Listen to ResetViews event.
const events = useCustomEvents();
const { resetViews } = storeToRefs(events);
watch(
resetViews, () => {
resetCamera();
});
watch([baseImageRep, currentImageData],
() => {
const image = currentImageData.value;
const outlineRep = baseImageRep.value;
if (image && outlineRep) {
const outlineFilter = vtkImageDataOutlineFilter.newInstance();
outlineFilter.setInputData(image);
outlineFilter.setGenerateFaces(false);
outlineFilter.setGenerateLines(true);
outlineRep.getMapper().setInputConnection(outlineFilter.getOutputPort());
const actor = outlineRep.getActors()?.at(0);
if (actor) {
actor.getProperty().setLineWidth(3.0);
}
}
addResliceProxiesToView();
},
{ immediate: true }
);
return {
vtkContainerRef,
viewID,
active: false,
resetCamera,
};
},
});
</script>

<style scoped src="@/src/components/styles/vtk-view.css"></style>
<style scoped src="@/src/components/styles/utils.css"></style>

<style scoped>
.vtk-three-container {
background-color: black;
grid-template-columns: auto;
}
</style>
6 changes: 3 additions & 3 deletions src/components/VtkObliqueView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ import {
watchEffect,
} from 'vue';
import { storeToRefs } from 'pinia';
import { vec3 } from 'gl-matrix';
import { vec3, mat3 } from 'gl-matrix';
import { onKeyStroke } from '@vueuse/core';
import type { Matrix3x3, Vector3 } from '@kitware/vtk.js/types';
import type { Vector3 } from '@kitware/vtk.js/types';
import vtkMatrixBuilder from '@kitware/vtk.js/Common/Core/MatrixBuilder';
import { useResizeToFit } from '@src/composables/useResizeToFit';
import vtkLPSView2DProxy from '@src/vtk/LPSView2DProxy';
Expand Down Expand Up @@ -583,7 +583,7 @@ export default defineComponent({
if (curImageData.value) {
const d9 = curImageData.value.getDirection();
const mat = Array.from(d9) as Matrix3x3;
const mat = Array.from(d9) as mat3;
Object.values(planes).forEach((plane) => {
const {normal, viewUp: vup} = plane;
vec3.transformMat3(normal, normal, mat);
Expand Down
24 changes: 20 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ export const InitViewIDs: Record<string, string> = {
Coronal: 'Coronal',
Sagittal: 'Sagittal',
Axial: 'Axial',
Three: '3D',
ObliqueCoronal: 'ObliqueCoronal',
ObliqueSagittal: 'ObliqueSagittal',
ObliqueAxial: 'ObliqueAxial',
Three: '3D',
ObliqueThree: 'Oblique3D',
};

/**
Expand Down Expand Up @@ -73,6 +74,13 @@ export const InitViewSpecs: Record<string, ViewSpec> = {
viewUp: 'Superior',
},
},
[InitViewIDs.ObliqueThree]: {
viewType: 'Oblique3D',
props: {
viewDirection: 'Posterior',
viewUp: 'Superior',
},
},
};

/**
Expand Down Expand Up @@ -127,9 +135,17 @@ export const Layouts: Record<string, Layout> = [
},
{
name: 'Oblique Only',
direction: LayoutDirection.V,
// items: [InitViewIDs.ObliqueAxial, InitViewIDs.Axial],
items: [InitViewIDs.ObliqueAxial, InitViewIDs.ObliqueCoronal, InitViewIDs.ObliqueSagittal],
direction: LayoutDirection.H,
items: [
{
direction: LayoutDirection.V,
// items: [InitViewIDs.ObliqueCoronal],
items: [InitViewIDs.ObliqueCoronal, InitViewIDs.ObliqueThree],
},
{ direction: LayoutDirection.V,
items: [InitViewIDs.ObliqueSagittal, InitViewIDs.ObliqueAxial],
}
],
},
{
name: '3D Only',
Expand Down
1 change: 1 addition & 0 deletions src/core/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ViewProxyType {
Volume = 'View3D',
Slice = 'View2D',
Oblique = 'Oblique',
Oblique3D = 'Oblique3D',
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/io/state-file/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ const ViewType = z.union([
z.literal('2D'),
z.literal('3D'),
z.literal('Oblique'),
z.literal('Oblique3D'),
]) satisfies z.ZodType<ViewType>;

export type ViewConfig = z.infer<typeof ViewConfig>;
Expand Down
11 changes: 11 additions & 0 deletions src/shims-vtk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,14 @@ declare module '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget' {
// Just forwarding vtk-js's definition as default export:
export default vtkResliceCursorWidget;
}

declare module '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy' {
// import vtkGeometryRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/GeometryRepresentationProxy';
import vtkAbstractRepresentationProxy from '@kitware/vtk.js/Proxy/Core/AbstractRepresentationProxy';
export interface vtkGeometryRepresentationProxy extends vtkAbstractRepresentationProxy {
setVisibility(visible: boolean): boolean;
getVisibility(): boolean;
}

export default vtkGeometryRepresentationProxy;
}
Loading

0 comments on commit b34d440

Please sign in to comment.